PY-83529 Do not resolve to empty skeleton package modules

(cherry picked from commit 699b50ea5e2be24852fc1c07acb4d372e760a251)

GitOrigin-RevId: 7270c13b1973eb00d88c4f7f6c71ac1a54291428
This commit is contained in:
Petr
2025-11-12 17:28:36 +00:00
committed by intellij-monorepo-bot
parent c6222fa129
commit 3a58a09cbe
14 changed files with 103 additions and 25 deletions
@@ -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
@@ -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<RatedResolveResult> resolveMemberFromReferenceTypeProviders(@NotNull PsiElement parent,
@NotNull String referencedName) {
final var context = TypeEvalContext.codeInsightFallback(parent.getProject());
@@ -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<PsiFileSystemItem> getSubmodulesList(@Nullable PsiDirectory directory, @Nullable PsiElement anchor) {
private static @NotNull List<PsiFileSystemItem> getSubmodulesList(@NotNull PsiDirectory directory, @Nullable PsiElement anchor) {
final List<PsiFileSystemItem> 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<LookupElement> getSubModuleVariants(@Nullable PsiDirectory directory,
@NotNull PsiElement location,
@Nullable Set<? super String> namesAlready) {
if (directory == null) {
return Collections.emptyList();
}
final List<LookupElement> result = new ArrayList<>();
for (PsiFileSystemItem item : getSubmodulesList(directory, location)) {
List<PsiFileSystemItem> 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) {
@@ -0,0 +1,3 @@
import pkg.subpkg
print(pkg.subpkg.Foo)
@@ -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);
@@ -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);
});
});
}
}