Improve resolve/completion for package attributes (PY-14385, PY-14387, PY-14388, PY-14335)

* Outside of import statement do not suggest names of not imported
submodules for namespace packages (PY-14385). It has been done so for
normal packages (with __init__.py) already, now the same policy
applies to namespace packages.
* While searching for imported submodules, use only immediate
children of package, not arbitrary descendants (PY-14387).
* Names defined in __init__.py for indirectly imported (intermediate)
packages are included in completion list (PY-14388).
* Removed custom collecting of submodule names from PyModuleType,
use only PyModuleType#collectImportedSubmodules for this purpose.
As result duplicate undecorated entries are no longer included
in completion list for namespace packages (PY-14335).
This commit is contained in:
Mikhail Golubev
2014-11-14 16:25:30 +03:00
parent 8a75043458
commit 3b04af366a
22 changed files with 140 additions and 45 deletions
@@ -77,6 +77,7 @@ import com.jetbrains.python.refactoring.classes.PyDependenciesComparator;
import com.jetbrains.python.refactoring.classes.extractSuperclass.PyExtractSuperclassHelper;
import com.jetbrains.python.refactoring.classes.membersManager.PyMemberInfo;
import com.jetbrains.python.sdk.PythonSdkType;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -992,8 +993,9 @@ public class PyUtil {
} // don't touch non-dirs
}
@Contract("null -> null; !null -> !null")
@Nullable
public static PsiElement turnInitIntoDir(PsiElement target) {
public static PsiElement turnInitIntoDir(@Nullable PsiElement target) {
if (target instanceof PyFile && isPackage((PsiFile)target)) {
return ((PsiFile)target).getContainingDirectory();
}
@@ -15,17 +15,20 @@
*/
package com.jetbrains.python.psi.types;
import com.google.common.collect.Sets;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.util.Condition;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.QualifiedName;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ProcessingContext;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.psi.*;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.psi.AccessDirection;
import com.jetbrains.python.psi.PyExpression;
import com.jetbrains.python.psi.PyFile;
import com.jetbrains.python.psi.PyUtil;
import com.jetbrains.python.psi.impl.PyImportedModule;
import com.jetbrains.python.psi.resolve.PointInImport;
import com.jetbrains.python.psi.resolve.PyResolveContext;
import com.jetbrains.python.psi.resolve.RatedResolveResult;
import com.jetbrains.python.psi.resolve.ResolveImportUtil;
@@ -35,6 +38,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* @author yole
@@ -58,33 +62,37 @@ public class PyImportedModuleType implements PyType {
return new PyModuleType(file, myImportedModule).resolveMember(name, location, direction, resolveContext);
}
else if (resolved instanceof PsiDirectory) {
final List<PsiElement> elements = Collections.singletonList(ResolveImportUtil.resolveChild(resolved, name, null, true, true));
List<PsiElement> elements = Collections.singletonList(ResolveImportUtil.resolveChild(resolved, name, null, true, true));
if (location != null && ResolveImportUtil.getPointInImport(location) == PointInImport.NONE) {
final Set<PsiElement> imported = Sets.newHashSet(PyModuleType.collectImportedSubmodules((PsiDirectory)resolved, location));
elements = ContainerUtil.filter(elements, new Condition<PsiElement>() {
@Override
public boolean value(PsiElement element) {
return imported.contains(element);
}
});
}
return ResolveImportUtil.rateResults(elements);
}
return null;
}
public Object[] getCompletionVariants(String completionPrefix, PsiElement location, ProcessingContext context) {
List<LookupElement> result = new ArrayList<LookupElement>();
ScopeOwner scopeOwner = ScopeUtil.getScopeOwner(location);
assert scopeOwner != null;
final List<PyImportElement> importTargets = PyModuleType.getVisibleImports(scopeOwner);
final int imported = myImportedModule.getImportedPrefix().getComponentCount();
for (PyImportElement importTarget : importTargets) {
final QualifiedName qName = importTarget.getImportedQName();
if (qName != null && qName.matchesPrefix(myImportedModule.getImportedPrefix())) {
final List<String> components = qName.getComponents();
if (components.size() > imported) {
String module = components.get(imported);
result.add(LookupElementBuilder.create(module));
}
}
}
final List<LookupElement> result = new ArrayList<LookupElement>();
final PsiElement resolved = myImportedModule.resolve();
if (resolved instanceof PsiDirectory) {
if (resolved instanceof PyFile) {
final PyModuleType moduleType = new PyModuleType((PyFile)resolved, myImportedModule);
result.addAll(moduleType.getCompletionVariantsAsLookupElements(location, context, false, false));
}
else if (resolved instanceof PsiDirectory) {
final PsiDirectory dir = (PsiDirectory)resolved;
if (PyUtil.isPackage(dir, location)) {
result.addAll(PyModuleType.getSubModuleVariants(dir, location, null));
if (ResolveImportUtil.getPointInImport(location) != PointInImport.NONE) {
result.addAll(PyModuleType.getSubModuleVariants(dir, location, null));
}
else {
result.addAll(PyModuleType.collectImportedSubmodulesAsLookupElements(dir, location, context.get(CTX_NAMES)));
}
}
}
return ArrayUtil.toObjectArray(result);
@@ -29,7 +29,9 @@ import com.intellij.psi.*;
import com.intellij.psi.scope.PsiScopeProcessor;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.QualifiedName;
import com.intellij.util.Function;
import com.intellij.util.ProcessingContext;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.codeInsight.PyCustomMember;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
@@ -331,7 +333,7 @@ public class PyModuleType implements PyType { // Modules don't descend from obje
result.addAll(getSubModuleVariants(myModule.getContainingDirectory(), location, namesAlready));
}
else {
addImportedSubmodules(location, namesAlready, result);
result.addAll(collectImportedSubmodulesAsLookupElements(myModule, location, namesAlready));
}
}
return result;
@@ -355,27 +357,62 @@ public class PyModuleType implements PyType { // Modules don't descend from obje
return processor;
}
private void addImportedSubmodules(PsiElement location, Set<String> existingNames, List<LookupElement> result) {
PsiFile file = location.getContainingFile();
if (file instanceof PyFile) {
PyFile pyFile = (PyFile)file;
PsiElement moduleBase = PyUtil.isPackage(myModule) ? myModule.getContainingDirectory() : myModule;
for (PyImportElement importElement : pyFile.getImportTargets()) {
PsiElement target = PyUtil.turnInitIntoDir(importElement.resolve());
if (target != null && PsiTreeUtil.isAncestor(moduleBase, target, true)) {
LookupElement element = null;
if (target instanceof PsiFileSystemItem) {
element = buildFileLookupElement((PsiFileSystemItem) target, existingNames);
}
else if (target instanceof PsiNamedElement) {
element = LookupElementBuilder.createWithIcon((PsiNamedElement)target);
}
if (element != null) {
result.add(element);
}
}
}
@NotNull
public static List<LookupElement> collectImportedSubmodulesAsLookupElements(@NotNull PsiFileSystemItem pyPackage,
@NotNull PsiElement location,
@Nullable final Set<String> existingNames) {
return ContainerUtil.mapNotNull(collectImportedSubmodules(pyPackage, location),
new Function<PsiElement, LookupElement>() {
@Override
public LookupElement fun(PsiElement element) {
if (element instanceof PsiFileSystemItem) {
return buildFileLookupElement((PsiFileSystemItem)element, existingNames);
}
else if (element instanceof PsiNamedElement) {
return LookupElementBuilder.createWithIcon((PsiNamedElement)element);
}
return null;
}
});
}
@NotNull
public static List<PsiElement> collectImportedSubmodules(@NotNull PsiFileSystemItem pyPackage, @NotNull PsiElement location) {
final PsiElement parentAnchor;
if (pyPackage instanceof PyFile && PyUtil.isPackage(((PyFile)pyPackage))) {
parentAnchor = ((PyFile)pyPackage).getContainingDirectory();
}
else if (pyPackage instanceof PsiDirectory && PyUtil.isPackage(((PsiDirectory)pyPackage), location)) {
parentAnchor = pyPackage;
}
else {
throw new IllegalArgumentException("Package argument should be either standard Python package or namespace package");
}
final ScopeOwner scopeOwner = ScopeUtil.getScopeOwner(location);
if (scopeOwner == null) {
return Collections.emptyList();
}
final List<PsiElement> result = new ArrayList<PsiElement>();
nextImportElement:
for (PyImportElement importElement : getVisibleImports(scopeOwner)) {
PsiElement resolvedChild = PyUtil.turnInitIntoDir(importElement.resolve());
if (resolvedChild == null || !PsiTreeUtil.isAncestor(parentAnchor, resolvedChild, true)) {
continue;
}
QualifiedName importedQName = importElement.getImportedQName();
// Looking for strict child of parentAncestor
while (resolvedChild != null && resolvedChild.getParent() != parentAnchor) {
if (importedQName == null || importedQName.getComponentCount() <= 1) {
continue nextImportElement;
}
importedQName = importedQName.removeTail(1);
resolvedChild = PyUtil.turnInitIntoDir(ResolveImportUtil.resolveImportElement(importElement, importedQName));
}
ContainerUtil.addIfNotNull(result, resolvedChild);
}
return result;
}
public static List<LookupElement> getSubModuleVariants(final PsiDirectory directory,
@@ -0,0 +1,3 @@
import pkg1.m1
pkg1.foo
@@ -0,0 +1,3 @@
import pkg1.m1
pkg1.f<caret>
@@ -0,0 +1,3 @@
import nspkg1.m2
nspkg1.m2
@@ -0,0 +1,3 @@
import nspkg1.m2
nspkg1.m<caret>
@@ -0,0 +1,4 @@
import pkg1.subpkg1.m1
import pkg1
pkg1.<caret>
@@ -0,0 +1,5 @@
import nspkg1.m2
print(nspkg1.m2)
print(nspkg1.<warning descr="Cannot find reference 'm3' in 'imported module nspkg1'">m3</warning>)
print(nspkg1.<warning descr="Cannot find reference 'nssubpkg1' in 'imported module nspkg1'">nssubpkg1</warning>)
@@ -136,4 +136,9 @@ public class Py3CompletionTest extends PyTestCase {
myFixture.completeBasic();
myFixture.checkResultByFile("completion/" + getTestName(true) + "/nspkg1/a.after.py");
}
// PY-14385
public void testNotImportedSubmodulesOfNamespacePackage() {
doMultiFileTest();
}
}
@@ -707,4 +707,19 @@ public class PythonCompletionTest extends PyTestCase {
assertContainsElements(suggested, PyNames.FUNCTION_SPECIAL_ATTRIBUTES);
assertDoesntContain(suggested, PyNames.METHOD_SPECIAL_ATTRIBUTES);
}
// PY-14388
public void testAttributeOfIndirectlyImportedPackage() {
doMultiFileTest();
}
// PY-14387
public void testSubmoduleOfIndirectlyImportedPackage() {
myFixture.copyDirectoryToProject("completion/" + getTestName(true), "");
myFixture.configureByFile("a.py");
myFixture.completeBasic();
final List<String> suggested = myFixture.getLookupElementStrings();
assertNotNull(suggested);
assertSameElements(suggested, "VAR", "subpkg1");
}
}
@@ -111,4 +111,9 @@ public class Py3UnresolvedReferencesInspectionTest extends PyTestCase {
public void testAttributesOfUnresolvedTypeFile() {
doTest();
}
// PY-14385
public void testNotImportedSubmodulesOfNamespacePackage() {
doMultiFileTest("main.py");
}
}