From 2d2976a88933d95a2c2d3e0bfb2d67198744017b Mon Sep 17 00:00:00 2001 From: Mikhail Golubev Date: Thu, 9 Aug 2018 20:16:57 +0300 Subject: [PATCH] PY-31299 Migrate PyPsiFacade#createClassByQName implementation to PyResolveImportUtil Using PyTypeParser for this purpose is, first, too heavyweight since it does a lot of unnecessary work, and, second, introduces unexpected problems related to the fact that it was originally intended to resolve types in docstrings and thus too permissive and depends on surrounding context such as existing imports. --- .../com/jetbrains/python/psi/PyPsiFacade.java | 8 ++++++++ .../python/psi/impl/PyPsiFacadeImpl.java | 20 +++++++++++++++---- .../foo/__init__.py | 0 .../foo/bar.py | 2 ++ ...assByQNameDoesntDependOnExistingImports.py | 3 +++ .../src/__init__.py | 0 .../src/foo.py | 0 .../python/PyMultiFileResolveTest.java | 9 +++++++++ 8 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/foo/__init__.py create mode 100644 python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/foo/bar.py create mode 100644 python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/src/CreateClassByQNameDoesntDependOnExistingImports.py create mode 100644 python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/src/__init__.py create mode 100644 python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/src/foo.py diff --git a/python/psi-api/src/com/jetbrains/python/psi/PyPsiFacade.java b/python/psi-api/src/com/jetbrains/python/psi/PyPsiFacade.java index 0467d5546355..b7a2002cf7ae 100644 --- a/python/psi-api/src/com/jetbrains/python/psi/PyPsiFacade.java +++ b/python/psi-api/src/com/jetbrains/python/psi/PyPsiFacade.java @@ -61,6 +61,14 @@ public abstract class PyPsiFacade { @Nullable public abstract PyType parseTypeAnnotation(@NotNull String annotation, @NotNull PsiElement anchor); + /** + * Retrieve a top-level class by its qualified name. The name provided is supposed to be fully qualified absolute name + * of the class, neither relative to the containing file of the anchor element, nor dependent on its imports. + * The anchor element is needed only to detect the corresponding module and its SDK. + * + * @param qName qualified name of the required class + * @param anchor arbitrary element located in the same module/SDK as the required class + */ @Nullable public abstract PyClass createClassByQName(@NotNull String qName, @NotNull PsiElement anchor); diff --git a/python/src/com/jetbrains/python/psi/impl/PyPsiFacadeImpl.java b/python/src/com/jetbrains/python/psi/impl/PyPsiFacadeImpl.java index 81c26862eeaf..5c7e79311258 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyPsiFacadeImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyPsiFacadeImpl.java @@ -15,16 +15,21 @@ */ package com.jetbrains.python.psi.impl; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.util.QualifiedName; +import com.intellij.util.ObjectUtils; import com.jetbrains.python.psi.PyClass; import com.jetbrains.python.psi.PyPsiFacade; -import com.jetbrains.python.psi.PyUtil; -import com.jetbrains.python.psi.resolve.*; +import com.jetbrains.python.psi.resolve.PyQualifiedNameResolveContext; +import com.jetbrains.python.psi.resolve.PyResolveImportUtil; +import com.jetbrains.python.psi.resolve.QualifiedNameFinder; import com.jetbrains.python.psi.stubs.PyClassNameIndex; import com.jetbrains.python.psi.types.*; +import one.util.streamex.StreamEx; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -86,8 +91,15 @@ public class PyPsiFacadeImpl extends PyPsiFacade { @Nullable @Override public final PyClass createClassByQName(@NotNull final String qName, @NotNull final PsiElement anchor) { - final PyClassType classType = PyUtil.as(parseTypeAnnotation(qName, anchor), PyClassType.class); - return (classType != null ? classType.getPyClass() : null); + final Module module = ModuleUtilCore.findModuleForPsiElement(ObjectUtils.notNull(anchor.getContainingFile(), anchor)); + if (module == null) return null; + // Don't use PyResolveImportUtil.fromFoothold here as setting foothold file is going to affect resolve results + // particularly if the anchor element happens to be in the same file as the target class. + final PyQualifiedNameResolveContext resolveContext = PyResolveImportUtil.fromModule(module).copyWithMembers(); + return StreamEx.of(resolveQualifiedName(QualifiedName.fromDottedString(qName), resolveContext)) + .select(PyClass.class) + .findFirst() + .orElse(null); } @Nullable diff --git a/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/foo/__init__.py b/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/foo/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/foo/bar.py b/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/foo/bar.py new file mode 100644 index 000000000000..07d9e0efebfa --- /dev/null +++ b/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/foo/bar.py @@ -0,0 +1,2 @@ +class MyClass: + pass diff --git a/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/src/CreateClassByQNameDoesntDependOnExistingImports.py b/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/src/CreateClassByQNameDoesntDependOnExistingImports.py new file mode 100644 index 000000000000..3d606ec59207 --- /dev/null +++ b/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/src/CreateClassByQNameDoesntDependOnExistingImports.py @@ -0,0 +1,3 @@ +import foo + +print(foo) diff --git a/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/src/__init__.py b/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/src/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/src/foo.py b/python/testData/resolve/multiFile/createClassByQNameDoesntDependOnExistingImports/src/foo.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 1f77fe43ddaa..fa80a3c4ea03 100644 --- a/python/testSrc/com/jetbrains/python/PyMultiFileResolveTest.java +++ b/python/testSrc/com/jetbrains/python/PyMultiFileResolveTest.java @@ -588,4 +588,13 @@ public class PyMultiFileResolveTest extends PyMultiFileResolveTestCase { public void testIncompleteFromImport() { assertUnresolved(); } + + public void testCreateClassByQNameDoesntDependOnExistingImports() { + prepareTestDirectory(); + runWithSourceRoots(Collections.singletonList(myFixture.findFileInTempDir("src")), () -> { + myFixture.configureByFile("src/" + getTestName(false) + ".py"); + final PyPsiFacade facade = PyPsiFacade.getInstance(myFixture.getProject()); + assertNotNull(facade.createClassByQName("foo.bar.MyClass", myFixture.getFile())); + }); + } } \ No newline at end of file