diff --git a/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.kt b/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.kt index db4a9735972e..4b69c8ff6cfe 100644 --- a/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.kt +++ b/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.kt @@ -18,7 +18,9 @@ import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.util.Key import com.intellij.openapi.util.registry.Registry import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.findFile import com.intellij.profile.codeInspection.InspectionProjectProfileManager +import com.intellij.psi.PsiDirectory import com.intellij.psi.PsiElement import com.intellij.psi.PsiReference import com.intellij.psi.search.FilenameIndex @@ -26,6 +28,7 @@ import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.QualifiedName import com.intellij.util.containers.ContainerUtil +import com.jetbrains.python.PyNames import com.jetbrains.python.PyPsiBundle import com.jetbrains.python.PyPsiPackageUtil import com.jetbrains.python.ast.PyAstFromImportStatement @@ -161,11 +164,21 @@ class PyUnresolvedReferencesInspection : PyUnresolvedReferencesInspectionBase() } val resolveResult: List = resolveInRoot(qname, containingDirectory, context) - if (!resolveResult.isEmpty()) { - if (Registry.`is`("python.source.root.suggest.quickfix.auto.apply")) { + if (resolveResult.size != 1) { + continue + } + if (Registry.`is`("python.source.root.suggest.quickfix.auto.apply")) { + val resolvedPsi = resolveResult.first() + val isPsiDirectoryWithInitPy = resolvedPsi is PsiDirectory && resolvedPsi.getVirtualFile().findChild(PyNames.INIT_DOT_PY) == null + if (!isPsiDirectoryWithInitPy) { + // If we resolved to a directory, it must contain "__init__.py". Otherwise, we might have false positives. + // See PY-86985 + // Quick fix remains available in that case project.getService(PySourceRootDetectionService::class.java).onSourceRootDetected(containingDirectory) } - return PyMarkDirectoryAsSourceRootQuickFix(project, containingDirectory) + } + return PyMarkDirectoryAsSourceRootQuickFix(project, containingDirectory).also { + } } return null diff --git a/python/testData/quickFixes/PyMarkDirectoryAsSourceRootQuickFixTest/mysrc/foo/abc_folder_no_init.py b/python/testData/quickFixes/PyMarkDirectoryAsSourceRootQuickFixTest/mysrc/foo/abc_folder_no_init.py new file mode 100644 index 000000000000..a1ca1cd4f166 --- /dev/null +++ b/python/testData/quickFixes/PyMarkDirectoryAsSourceRootQuickFixTest/mysrc/foo/abc_folder_no_init.py @@ -0,0 +1 @@ +import qwerty \ No newline at end of file diff --git a/python/testData/quickFixes/PyMarkDirectoryAsSourceRootQuickFixTest/mysrc/foo/abc_folder_no_init_after.py b/python/testData/quickFixes/PyMarkDirectoryAsSourceRootQuickFixTest/mysrc/foo/abc_folder_no_init_after.py new file mode 100644 index 000000000000..1a0ace5f21c8 --- /dev/null +++ b/python/testData/quickFixes/PyMarkDirectoryAsSourceRootQuickFixTest/mysrc/foo/abc_folder_no_init_after.py @@ -0,0 +1 @@ +import qwerty \ No newline at end of file diff --git a/python/testData/quickFixes/PyMarkDirectoryAsSourceRootQuickFixTest/mysrc/foo/abc_folder_with_init.py b/python/testData/quickFixes/PyMarkDirectoryAsSourceRootQuickFixTest/mysrc/foo/abc_folder_with_init.py new file mode 100644 index 000000000000..59da5fda7435 --- /dev/null +++ b/python/testData/quickFixes/PyMarkDirectoryAsSourceRootQuickFixTest/mysrc/foo/abc_folder_with_init.py @@ -0,0 +1 @@ +import qwerty_with_init \ No newline at end of file diff --git a/python/testData/quickFixes/PyMarkDirectoryAsSourceRootQuickFixTest/mysrc/foo/abc_folder_with_init_after.py b/python/testData/quickFixes/PyMarkDirectoryAsSourceRootQuickFixTest/mysrc/foo/abc_folder_with_init_after.py new file mode 100644 index 000000000000..59da5fda7435 --- /dev/null +++ b/python/testData/quickFixes/PyMarkDirectoryAsSourceRootQuickFixTest/mysrc/foo/abc_folder_with_init_after.py @@ -0,0 +1 @@ +import qwerty_with_init \ No newline at end of file diff --git a/python/testData/quickFixes/PyMarkDirectoryAsSourceRootQuickFixTest/mysrc/qwerty_with_init/__init__.py b/python/testData/quickFixes/PyMarkDirectoryAsSourceRootQuickFixTest/mysrc/qwerty_with_init/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testSrc/com/jetbrains/python/quickFixes/PyMarkDirectoryAsSourceRootQuickFixTest.kt b/python/testSrc/com/jetbrains/python/quickFixes/PyMarkDirectoryAsSourceRootQuickFixTest.kt index ae27b8b6c013..cb6c6799f822 100644 --- a/python/testSrc/com/jetbrains/python/quickFixes/PyMarkDirectoryAsSourceRootQuickFixTest.kt +++ b/python/testSrc/com/jetbrains/python/quickFixes/PyMarkDirectoryAsSourceRootQuickFixTest.kt @@ -100,6 +100,23 @@ class PyMarkDirectoryAsSourceRootQuickFixTest: PyQuickFixTestCase() { testSourceRoot(expectedSourceRootPaths = emptySet()) } + fun testNoQuickFixBecauseResolvedToFolderWithoutInitPy() { + testSourceRoot(expectedSourceRootPaths = emptySet()) + openAndHighlightFile("mysrc/foo/abc_folder_no_init.py") + testSourceRoot(expectedSourceRootPaths = emptySet()) + findAndExecuteSourcesQuickFix(isQuickFixExpected = true) + testSourceRoot(expectedSourceRootPaths = setOf("/src/mysrc")) + } + + fun testNoQuickFixBecauseResolvedToFolderWithInitPy() { + testSourceRoot(expectedSourceRootPaths = emptySet()) + openAndHighlightFile("mysrc/foo/abc_folder_with_init.py") + testSourceRoot(expectedSourceRootPaths = setOf("/src/mysrc")) + // quick fix is not expected because it will be already automatically applied + findAndExecuteSourcesQuickFix(isQuickFixExpected = false) + testSourceRoot(expectedSourceRootPaths = setOf("/src/mysrc")) + } + private fun testSourceRoot(expectedSourceRootPaths: Set) { val moduleRootManager = ModuleRootManager.getInstance(myFixture.module) val detectedSourceRoots = moduleRootManager.sourceRoots.map {