diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyResolveImportUtil.kt b/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyResolveImportUtil.kt index c0df5b8c065c..7a27e2eccd3f 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyResolveImportUtil.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/PyResolveImportUtil.kt @@ -68,7 +68,7 @@ fun resolveQualifiedName(name: QualifiedName, context: PyQualifiedNameResolveCon val key = cachePrefix(context).append(name) if (mayCache) { - val cachedResults = cache?.get(key) + val cachedResults = cache.get(key) if (cachedResults != null) { return (relativeResults + cachedResults).distinct() } @@ -87,15 +87,19 @@ fun resolveQualifiedName(name: QualifiedName, context: PyQualifiedNameResolveCon } } - val results = if (relativeDirectory != null && PyUtil.isExplicitPackage(relativeDirectory)) { + val allResults = if (relativeDirectory != null && PyUtil.isExplicitPackage(relativeDirectory)) { filterTopPriorityResultsWithFallback(notSameDirectoryPython3Results, sameDirectoryPython3Results, foreignResults, name, context) } else { filterTopPriorityResultsWithFallback(sameDirectoryPython3Results, notSameDirectoryPython3Results, foreignResults, name, context) } + val results = allResults + .filterNot { it is PsiFileSystemItem && isInSkeletons(it) } + .ifEmpty { allResults } + if (mayCache) { - cache?.put(key, results) + cache.put(key, results) } return results diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/ResolveImportUtil.java b/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/ResolveImportUtil.java index 0f9b8d78fd95..e11b92bde054 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/ResolveImportUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/resolve/ResolveImportUtil.java @@ -10,6 +10,8 @@ import com.intellij.openapi.util.RecursionManager; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.util.registry.Registry; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; @@ -27,6 +29,8 @@ import com.jetbrains.python.psi.types.PyType; import com.jetbrains.python.psi.types.TypeEvalContext; import com.jetbrains.python.pyi.PyiStubSuppressor; import com.jetbrains.python.pyi.PyiUtil; +import com.jetbrains.python.sdk.PythonSdkUtil; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -319,13 +323,22 @@ public final class ResolveImportUtil { } } } - if (!withoutForeign && parent instanceof PsiFile) { - final PsiElement foreign = resolveForeignImports((PsiFile)parent, referencedName); - if (foreign != null) { - final ResolveResultList results = new ResolveResultList(); - results.addAll(resolved); - results.poke(foreign, RatedResolveResult.RATE_NORMAL); - return results; + if (!withoutForeign) { + if (parent instanceof PsiFile) { + final PsiElement foreign = resolveForeignImports((PsiFile)parent, referencedName); + if (foreign != null) { + final ResolveResultList results = new ResolveResultList(); + results.addAll(resolved); + results.poke(foreign, RatedResolveResult.RATE_NORMAL); + return results; + } + } + else if (parent instanceof PsiDirectory dir) { + PsiDirectory skeletonsDir = findCorrespondingSkeletonsDir(dir); + if (skeletonsDir != null) { + return ContainerUtil.concat(resolved, resolveInDirectory(referencedName, containingFile, skeletonsDir, fileOnly, + checkForPackage, withoutStubs)); + } } } return resolved; @@ -339,6 +352,28 @@ public final class ResolveImportUtil { return !results.isEmpty() ? results.get(0) : null; } + @ApiStatus.Internal + public static @Nullable PsiDirectory findCorrespondingSkeletonsDir(@NotNull PsiDirectory packageDir) { + Sdk sdk = PythonSdkUtil.findPythonSdk(packageDir); + if (sdk == null) return null; + + VirtualFile skeletonsDir = PythonSdkUtil.findSkeletonsDir(sdk); + if (skeletonsDir == null) return null; + + boolean isInSkeletons = VfsUtilCore.isAncestor(skeletonsDir, packageDir.getVirtualFile(), false); + if (isInSkeletons) return null; + + PsiDirectory skeletonsPsiDir = packageDir.getManager().findDirectory(skeletonsDir); + if (skeletonsPsiDir == null) return null; + + QualifiedName packageName = QualifiedNameFinder.findShortestImportableQName(packageDir); + if (packageName == null) return null; + + List<@NotNull PsiElement> elements = + PyResolveImportUtil.resolveModuleAt(packageName, skeletonsPsiDir, PyResolveImportUtil.fromFoothold(skeletonsPsiDir)); + return ContainerUtil.getFirstItem(elements) instanceof PsiDirectory dir ? dir : null; + } + private static @NotNull List resolveMemberFromReferenceTypeProviders(@NotNull PsiElement parent, @NotNull String referencedName) { final var context = TypeEvalContext.codeInsightFallback(parent.getProject()); diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyModuleType.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyModuleType.java index f34a47ef1b4d..479c5371ed30 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyModuleType.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyModuleType.java @@ -381,23 +381,21 @@ public class PyModuleType implements PyType { // Modules don't descend from obje * @return a list of submodules of the specified module directory, either files or dirs, for easier naming; may contain file names * not suitable for import. */ - private static @NotNull List getSubmodulesList(@Nullable PsiDirectory directory, @Nullable PsiElement anchor) { + private static @NotNull List getSubmodulesList(@NotNull PsiDirectory directory, @Nullable PsiElement anchor) { final List result = new ArrayList<>(); - if (directory != null) { // just in case - // file modules - for (PsiFile f : directory.getFiles()) { - final String filename = f.getName(); - // if we have a binary module, we'll most likely also have a stub for it in site-packages - if (!isExcluded(f) && (f instanceof PyFile && !filename.equals(PyNames.INIT_DOT_PY)) || isBinaryModule(filename)) { - result.add(f); - } + // file modules + for (PsiFile f : directory.getFiles()) { + final String filename = f.getName(); + // if we have a binary module, we'll most likely also have a stub for it in site-packages + if (!isExcluded(f) && (f instanceof PyFile && !filename.equals(PyNames.INIT_DOT_PY)) || isBinaryModule(filename)) { + result.add(f); } - // dir modules - for (PsiDirectory dir : directory.getSubdirectories()) { - if (!isExcluded(dir) && PyUtil.isPackage(dir, anchor)) { - result.add(dir); - } + } + // dir modules + for (PsiDirectory dir : directory.getSubdirectories()) { + if (!isExcluded(dir) && PyUtil.isPackage(dir, anchor)) { + result.add(dir); } } return result; @@ -530,8 +528,16 @@ public class PyModuleType implements PyType { // Modules don't descend from obje public static @NotNull List getSubModuleVariants(@Nullable PsiDirectory directory, @NotNull PsiElement location, @Nullable Set namesAlready) { + if (directory == null) { + return Collections.emptyList(); + } final List result = new ArrayList<>(); - for (PsiFileSystemItem item : getSubmodulesList(directory, location)) { + List items = getSubmodulesList(directory, location); + PsiDirectory skeletonsDir = ResolveImportUtil.findCorrespondingSkeletonsDir(directory); + if (skeletonsDir != null) { + items = ContainerUtil.concat(items, getSubmodulesList(skeletonsDir, location)); + } + for (PsiFileSystemItem item : items) { if (item != location.getContainingFile().getOriginalFile()) { final LookupElement lookupElement = buildFileLookupElement(location.getContainingFile(), item, namesAlready); if (lookupElement != null) { diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PackageAttributeInPresenceOfBinarySkeleton/main.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PackageAttributeInPresenceOfBinarySkeleton/main.py new file mode 100644 index 000000000000..a1c713699342 --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PackageAttributeInPresenceOfBinarySkeleton/main.py @@ -0,0 +1,3 @@ +import pkg.subpkg + +print(pkg.subpkg.Foo) \ No newline at end of file diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PackageAttributeInPresenceOfBinarySkeleton/python_stubs/pkg/__init__.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PackageAttributeInPresenceOfBinarySkeleton/python_stubs/pkg/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PackageAttributeInPresenceOfBinarySkeleton/python_stubs/pkg/subpkg/__init__.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PackageAttributeInPresenceOfBinarySkeleton/python_stubs/pkg/subpkg/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PackageAttributeInPresenceOfBinarySkeleton/python_stubs/pkg/subpkg/binary.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PackageAttributeInPresenceOfBinarySkeleton/python_stubs/pkg/subpkg/binary.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PackageAttributeInPresenceOfBinarySkeleton/site-packages/pkg/subpkg/__init__.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PackageAttributeInPresenceOfBinarySkeleton/site-packages/pkg/subpkg/__init__.py new file mode 100644 index 000000000000..a2f1ddf1149b --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PackageAttributeInPresenceOfBinarySkeleton/site-packages/pkg/subpkg/__init__.py @@ -0,0 +1 @@ +from pkg.subpkg.subpkg import Foo diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/PackageAttributeInPresenceOfBinarySkeleton/site-packages/pkg/subpkg/subpkg.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PackageAttributeInPresenceOfBinarySkeleton/site-packages/pkg/subpkg/subpkg.py new file mode 100644 index 000000000000..8dd3b4782855 --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/PackageAttributeInPresenceOfBinarySkeleton/site-packages/pkg/subpkg/subpkg.py @@ -0,0 +1 @@ +Foo = ... \ No newline at end of file diff --git a/python/testData/types/ImportNestedBinarySubModule/python_stubs/pkg/__init__.py b/python/testData/types/ImportNestedBinarySubModule/python_stubs/pkg/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/types/ImportNestedBinarySubModule/python_stubs/pkg/subpkg/__init__.py b/python/testData/types/ImportNestedBinarySubModule/python_stubs/pkg/subpkg/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/types/ImportNestedBinarySubModule/site-packages/pkg/subpkg/__init__.py b/python/testData/types/ImportNestedBinarySubModule/site-packages/pkg/subpkg/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testSrc/com/jetbrains/python/Py3TypeTest.java b/python/testSrc/com/jetbrains/python/Py3TypeTest.java index eb11ce694b84..462506339764 100644 --- a/python/testSrc/com/jetbrains/python/Py3TypeTest.java +++ b/python/testSrc/com/jetbrains/python/Py3TypeTest.java @@ -4179,6 +4179,23 @@ public class Py3TypeTest extends PyTestCase { """); } + // PY-83529 + public void testImportNestedBinarySubModule() { + String testDir = TEST_DIRECTORY + getTestName(false); + runWithAdditionalClassEntryInSdkRoots(testDir + "/site-packages", () -> { + runWithAdditionalClassEntryInSdkRoots(testDir + "/python_stubs", () -> { + doTest("imported module pkg", """ + import pkg.subpkg + expr = pkg + """); + doTest("__init__.py", """ + import pkg.subpkg + expr = pkg.subpkg + """); + }); + }); + } + private void doTest(final String expectedType, final String text) { myFixture.configureByText(PythonFileType.INSTANCE, text); final PyExpression expr = myFixture.findElementByText("expr", PyExpression.class); diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java index 255078ae97c0..b8763ba05f4f 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java @@ -491,4 +491,15 @@ public class Py3UnresolvedReferencesInspectionTest extends PyInspectionTestCase public void testStrictUnionMemberExtendingAny() { doTest(); } + + // PY-83529 + public void testPackageAttributeInPresenceOfBinarySkeleton() { + runWithAdditionalClassEntryInSdkRoots(getTestDirectoryPath() + "/site-packages", () -> { + runWithAdditionalClassEntryInSdkRoots(getTestDirectoryPath() + "/python_stubs", () -> { + final PsiFile currentFile = myFixture.configureByFile(getTestDirectoryPath() + "/main.py"); + configureInspection(); + assertSdkRootsNotParsed(currentFile); + }); + }); + } }