diff --git a/python/psi-api/src/com/jetbrains/python/psi/impl/PyPsiUtils.java b/python/psi-api/src/com/jetbrains/python/psi/impl/PyPsiUtils.java index 8dba1d02da9b..46f07970447a 100644 --- a/python/psi-api/src/com/jetbrains/python/psi/impl/PyPsiUtils.java +++ b/python/psi-api/src/com/jetbrains/python/psi/impl/PyPsiUtils.java @@ -19,7 +19,6 @@ import com.google.common.base.Preconditions; import com.intellij.lang.ASTNode; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; -import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.Couple; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; @@ -604,6 +603,14 @@ public class PyPsiUtils { Preconditions.checkArgument(!module.isDisposed(), String.format("Module %s is disposed", module)); } + @NotNull + public static PsiFileSystemItem getFileSystemItem(@NotNull PsiElement element) { + if (element instanceof PsiFileSystemItem) { + return (PsiFileSystemItem)element; + } + return element.getContainingFile(); + } + private static abstract class TopLevelVisitor extends PyRecursiveElementVisitor { public void visitPyElement(final PyElement node) { super.visitPyElement(node); diff --git a/python/src/com/jetbrains/python/psi/resolve/QualifiedNameResolverImpl.java b/python/src/com/jetbrains/python/psi/resolve/QualifiedNameResolverImpl.java index 5592b58c61c5..58b594e264a1 100644 --- a/python/src/com/jetbrains/python/psi/resolve/QualifiedNameResolverImpl.java +++ b/python/src/com/jetbrains/python/psi/resolve/QualifiedNameResolverImpl.java @@ -28,15 +28,13 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.roots.FileIndexFacade; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.PsiDirectory; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiNamedElement; +import com.intellij.psi.*; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.QualifiedName; import com.jetbrains.python.codeInsight.userSkeletons.PyUserSkeletonsUtil; import com.jetbrains.python.console.PydevConsoleRunner; import com.jetbrains.python.facet.PythonPathContributingFacet; +import com.jetbrains.python.psi.LanguageLevel; import com.jetbrains.python.psi.PyFile; import com.jetbrains.python.psi.PyUtil; import com.jetbrains.python.psi.impl.PyBuiltinCache; @@ -44,6 +42,7 @@ import com.jetbrains.python.psi.impl.PyImportResolver; import com.jetbrains.python.psi.impl.PyPsiUtils; import com.jetbrains.python.sdk.PySdkUtil; import com.jetbrains.python.sdk.PythonSdkType; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -52,6 +51,8 @@ import java.util.Collections; import java.util.List; import java.util.Set; +import static com.jetbrains.python.psi.PyUtil.as; + /** * Resolves the specified qualified name in the specified context (module, all modules or a file) to a file or directory. * @@ -235,6 +236,7 @@ public class QualifiedNameResolverImpl implements RootVisitor, QualifiedNameReso final PsiFile footholdFile = myContext.getFootholdFile(); checkValidForTests(footholdFile); + boolean foundRelativeImport = false; if (myRelativeLevel >= 0 && footholdFile != null && !PyUserSkeletonsUtil.isUnderUserSkeletonsDirectory(footholdFile)) { PsiDirectory dir = footholdFile.getContainingDirectory(); checkValidForTests(dir); @@ -246,12 +248,13 @@ public class QualifiedNameResolverImpl implements RootVisitor, QualifiedNameReso PsiElement module = resolveModuleAt(dir); checkValidForTests(module); if (module != null) { + foundRelativeImport = isRelativeImportResult(dir, module); addRoot(module, true); } } final PythonPathCache cache = findMyCache(); - final boolean mayCache = cache != null && !myWithoutRoots && !myWithoutForeign; + final boolean mayCache = cache != null && !myWithoutRoots && !myWithoutForeign && !foundRelativeImport; if (mayCache) { final List cachedResults = cache.get(myQualifiedName); if (cachedResults != null) { @@ -293,6 +296,22 @@ public class QualifiedNameResolverImpl implements RootVisitor, QualifiedNameReso return results; } + + private boolean isRelativeImportResult(@NotNull PsiDirectory rootDirectory, @NotNull PsiElement result) { + if (myRelativeLevel > 0) { + return true; + } + final boolean isPython2 = LanguageLevel.forElement(rootDirectory).isOlderThan(LanguageLevel.PYTHON30); + if (myRelativeLevel == 0 && isPython2 && PyUtil.isPackage(rootDirectory, false, null)) { + // Candidate for implicit relative import doesn't necessarily means that the same module cannot be imported absolutely + final PsiFileSystemItem moduleOrPackage = as(result, PsiFileSystemItem.class); + if (moduleOrPackage != null && !myQualifiedName.equals(QualifiedNameFinder.findShortestImportableQName(moduleOrPackage))) { + return true; + } + } + return false; + } + /** * Resolve relative imports from sdk root to the skeleton dir */ @@ -421,6 +440,7 @@ public class QualifiedNameResolverImpl implements RootVisitor, QualifiedNameReso * * @param directory where to start from; top qualifier will be searched for here. */ + @Contract("null -> null") @Nullable public PsiElement resolveModuleAt(@Nullable PsiDirectory directory) { // prerequisites diff --git a/python/testData/resolve/multiFile/ambiguousImplicitRelativeImport/pkg/__init__.py b/python/testData/resolve/multiFile/ambiguousImplicitRelativeImport/pkg/__init__.py new file mode 100644 index 000000000000..3074a5c357d1 --- /dev/null +++ b/python/testData/resolve/multiFile/ambiguousImplicitRelativeImport/pkg/__init__.py @@ -0,0 +1,2 @@ +import mod + \ No newline at end of file diff --git a/python/testData/resolve/multiFile/ambiguousImplicitRelativeImport/pkg/mod.py b/python/testData/resolve/multiFile/ambiguousImplicitRelativeImport/pkg/mod.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/resolve/multiFile/ambiguousImplicitRelativeImport/pkg2/__init__.py b/python/testData/resolve/multiFile/ambiguousImplicitRelativeImport/pkg2/__init__.py new file mode 100644 index 000000000000..3074a5c357d1 --- /dev/null +++ b/python/testData/resolve/multiFile/ambiguousImplicitRelativeImport/pkg2/__init__.py @@ -0,0 +1,2 @@ +import mod + \ No newline at end of file diff --git a/python/testData/resolve/multiFile/ambiguousImplicitRelativeImport/pkg2/mod.py b/python/testData/resolve/multiFile/ambiguousImplicitRelativeImport/pkg2/mod.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testSrc/com/jetbrains/python/PyMultiFileResolveTest.java b/python/testSrc/com/jetbrains/python/PyMultiFileResolveTest.java index c0294660b7ff..6d61dfaed139 100644 --- a/python/testSrc/com/jetbrains/python/PyMultiFileResolveTest.java +++ b/python/testSrc/com/jetbrains/python/PyMultiFileResolveTest.java @@ -18,6 +18,8 @@ package com.jetbrains.python; import com.google.common.collect.Lists; import com.intellij.openapi.module.Module; import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.roots.ModuleRootManager; +import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.impl.source.PsiFileImpl; @@ -26,10 +28,14 @@ import com.jetbrains.python.fixtures.PyMultiFileResolveTestCase; import com.jetbrains.python.fixtures.PyResolveTestCase; import com.jetbrains.python.fixtures.PyTestCase; import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.impl.PyPsiUtils; import com.jetbrains.python.sdk.PythonSdkType; import org.jetbrains.annotations.NotNull; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author yole @@ -459,4 +465,29 @@ public class PyMultiFileResolveTest extends PyMultiFileResolveTestCase { } } } + + // PY-19989 + public void testAmbiguousImplicitRelativeImport() { + prepareTestDirectory(); + assertSameElements(doMultiResolveAndGetFileUrls("pkg2/__init__.py"), "pkg2/mod.py"); + assertSameElements(doMultiResolveAndGetFileUrls("pkg/__init__.py"), "pkg/mod.py"); + } + + @NotNull + private List doMultiResolveAndGetFileUrls(@NotNull String currentFilePath) { + myFixture.configureByFile(currentFilePath); + final PsiReference reference = PyResolveTestCase.findReferenceByMarker(myFixture.getFile()); + final VirtualFile root = ModuleRootManager.getInstance(myFixture.getModule()).getSourceRoots()[0]; + + final Stream fileSystemItems; + if (reference instanceof PsiPolyVariantReference) { + final ResolveResult[] results = ((PsiPolyVariantReference)reference).multiResolve(false); + fileSystemItems = Arrays.stream(results).map(r -> PyPsiUtils.getFileSystemItem(r.getElement())); + } + else { + fileSystemItems = Stream.of(PyPsiUtils.getFileSystemItem(reference.resolve())); + } + return fileSystemItems.map(f -> VfsUtilCore.getRelativeLocation(f.getVirtualFile(), root)).collect(Collectors.toList()); + + } } \ No newline at end of file