A more complete fix for PY-147. Proposes several ways to import potentially importable names,

adds necessary import statements, handles name clashes.
       Also, bits of semi-related refactoring here and there.
This commit is contained in:
Dmitry Cheryasov
2009-04-27 08:33:14 +04:00
parent 11be0689a1
commit 61463799aa
13 changed files with 488 additions and 173 deletions
@@ -137,4 +137,4 @@ runcfg.labels.interpreter_options=Interpreter &options:
runcfg.labels.working_directory=&Working directory:
runcfg.captions.script_parameters_dialog=Enter script parameters
runcfg.captions.interpreter_options_dialog=Enter interpreter options
ACT.qualify.with.module=Qualify with module
ACT.qualify.with.module=Qualify with an imported module
@@ -11,25 +11,25 @@ import com.intellij.codeInspection.HintAction;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiReference;
import com.intellij.psi.ResolveState;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.PythonLanguage;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.ResolveImportUtil;
import com.jetbrains.python.psi.resolve.PyResolveUtil;
import com.jetbrains.python.psi.resolve.PyClassScopeProcessor;
import com.jetbrains.python.psi.resolve.PyResolveUtil;
import com.jetbrains.python.psi.resolve.ResolveImportUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class AddImportAction implements HintAction, QuestionAction, LocalQuickFix {
private final PsiReference myReference;
private final Project myProject;
private static final Logger LOG = Logger.getInstance("#" + AddImportAction.class.getName());
public AddImportAction(final PsiReference reference) {
myReference = reference;
@@ -203,42 +203,14 @@ public class AddImportAction implements HintAction, QuestionAction, LocalQuickFi
private void execute(final PsiFile file) {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
public void run() {
final String referenceName = getRefName();
if (ResolveImportUtil.resolveInRoots(file, referenceName) != null) {
// TODO: annotate the case of multiple files
final PyImportStatement importNodeToInsert = PythonLanguage.getInstance().getElementGenerator().createImportStatementFromText(
myProject, "import " + referenceName + "\n\n"
);
try {
file.addBefore(importNodeToInsert, getInsertPosition(file));
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
String name = getRefName();
if (ResolveImportUtil.resolveInRoots(file, name) != null) { // TODO: think about multiple possible resole results
AddImportHelper.addImportStatement(file, name, null, file.getProject());
}
}
});
}
private static PsiElement getInsertPosition(final PsiFile file) {
PsiElement feeler = file.getFirstChild();
LOG.assertTrue(feeler != null);
// skip initial comments and whitespace and try to get just below the last import stmt
PsiElement seeker = feeler;
do {
if (PyUtil.instanceOf(feeler, PyImportStatement.class, PyFromImportStatement.class)) {
seeker = feeler;
feeler = feeler.getNextSibling();
}
else if (PyUtil.instanceOf(feeler, PsiWhiteSpace.class, PsiComment.class)) {
seeker = feeler;
feeler = feeler.getNextSibling();
}
else break; // some other statement, stop
} while (feeler != null);
return seeker;
}
public boolean startInWriteAction() {
return false;
}
@@ -0,0 +1,66 @@
package com.jetbrains.python.actions;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.util.IncorrectOperationException;
import com.jetbrains.python.PythonLanguage;
import com.jetbrains.python.psi.PyFromImportStatement;
import com.jetbrains.python.psi.PyImportStatement;
import com.jetbrains.python.psi.PyUtil;
/**
* Does the actual job of adding an import statement into a file.
* User: dcheryasov
* Date: Apr 24, 2009 3:17:59 AM
*/
// intentional package level access
class AddImportHelper {
private static final Logger LOG = Logger.getInstance("#" + AddImportHelper.class.getName());
private static PsiElement getInsertPosition(final PsiFile file) {
PsiElement feeler = file.getFirstChild();
LOG.assertTrue(feeler != null);
// skip initial comments and whitespace and try to get just below the last import stmt
PsiElement seeker = feeler;
do {
if (PyUtil.instanceOf(feeler, PyImportStatement.class, PyFromImportStatement.class)) {
seeker = feeler;
feeler = feeler.getNextSibling();
}
else if (PyUtil.instanceOf(feeler, PsiWhiteSpace.class, PsiComment.class)) {
seeker = feeler;
feeler = feeler.getNextSibling();
}
else break; // some other statement, stop
} while (feeler != null);
return seeker;
}
/**
* Adds an import statement, presumably below all other initial imports in the file.
* @param file where to operate
* @param name which to import (qualified is OK)
* @param asName optional na,e for 'as' clause
* @param project to which the file presumably belongs
*/
public static void addImportStatement(PsiFile file, String name, String asName, Project project) {
String as_clause;
if (asName == null) as_clause = "";
else as_clause = " as " + asName;
final PyImportStatement importNodeToInsert = PythonLanguage.getInstance().getElementGenerator().createImportStatementFromText(
project, "import " + name + as_clause + "\n\n"
);
try {
file.addBefore(importNodeToInsert, getInsertPosition(file));
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
}
}
@@ -0,0 +1,123 @@
package com.jetbrains.python.actions;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.jetbrains.python.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* An immutable holder of information for one auto-import candidate.
* User: dcheryasov
* Date: Apr 23, 2009 4:17:50 PM
*/
// visibility is intentionally package-level
class ImportCandidateHolder {
private final PsiElement myImportable;
private final PyImportElement myImportElement;
private final PsiFile myFile;
private final String myPath;
private final String myAsName;
/**
* Creates new instance.
* @param importable an element that could be imported either from import element or from file.
* @param file the file which is the source of the importable
* @param importElement an existing import element that can be a source for the importable.
* @param path import path for the file, as a qualified name (a.b.c)
* @param asName name to use in a new import statement for 'as' clause, if an import is added.
*/
public ImportCandidateHolder(
@NotNull PsiElement importable, @NotNull PsiFile file,
@Nullable PyImportElement importElement, @Nullable String path, @Nullable String asName
) {
myFile = file;
myImportable = importable;
myImportElement = importElement;
myPath = path;
myAsName = asName;
assert importElement != null || path != null; // one of these must be present
}
public PsiElement getImportable() {
return myImportable;
}
public PyImportElement getImportElement() {
return myImportElement;
}
public PsiFile getFile() {
return myFile;
}
public String getPath() {
return myPath;
}
public String getAsName() {
return myAsName;
}
/**
* Helper method that builds an import path, handling all these "import foo", "import foo as bar", "from bar import foo", etc.
* Either importPath or importSource must be not null.
* @param name what is ultimately imported.
* @param importPath known path to import the name.
* @param source known ImportElement to import the name; its 'as' clause is used if present.
* @return a properly qualified name.
*/
public static String getQualifiedName(String name, String importPath, PyImportElement source) {
StringBuffer sb = new StringBuffer();
PsiElement parent = null;
if (source != null) {
parent = source.getParent();
if (parent instanceof PyFromImportStatement) {
sb.append(name);
}
else {
sb.append(source.getVisibleName()).append(".").append(name);
}
}
else {
sb.append(importPath).append(".").append(name);
}
return sb.toString();
}
public String getPresentableText(String myName) {
StringBuffer sb = new StringBuffer(getQualifiedName(myName, myPath, myImportElement));
PsiElement parent = null;
if (myImportElement != null) {
parent = myImportElement.getParent();
}
if (myImportable instanceof PyFunction) {
sb.append("(");
// below: ", ".join([x.getRepr(False) for x in getParameters()])
PyParameter[] params = ((PyFunction)myImportable).getParameterList().getParameters();
String[] param_reprs = new String[params.length];
for (int i=0; i < params.length; i += 1) param_reprs[i] = params[i].getRepr(false);
PyUtil.joinSubarray(param_reprs, 0, params.length, ", ", sb);
sb.append(")");
}
else if (myImportable instanceof PyClass) {
PyClass[] supers = ((PyClass)myImportable).getSuperClasses();
if (supers.length > 0) {
sb.append("(");
// ", ".join(x.getName() for x in getSuperClasses())
String[] super_names = new String[supers.length];
for (int i=0; i < supers.length; i += 1) super_names[i] = supers[i].getName();
PyUtil.joinSubarray(super_names, 0, supers.length, ", ", sb);
sb.append(")");
}
}
if (parent instanceof PyFromImportStatement) {
sb.append(" from ").append(((PyFromImportStatement)parent).getImportSource().getReferencedName()); // no NPE, we won't add a sourceless import stmt
}
else if (myImportElement == null) { // no import, only file
sb.append(" # add import");
if (myAsName != null) sb.append(" as ").append(myAsName);
}
return sb.toString();
}
}
@@ -6,9 +6,8 @@ import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.ui.popup.PopupChooserBuilder;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.PopupChooserBuilder;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.ui.SimpleColoredComponent;
@@ -30,15 +29,17 @@ import java.util.List;
public class ImportFromExistingAction implements QuestionAction {
PyElement myTarget;
List<Pair<PyImportElement, PsiElement>> mySources; // list of <import, imported_item>
List<ImportCandidateHolder> mySources; // list of <import, imported_item>
Editor myEditor;
String myName;
/**
* @param target element to become qualified as imported.
* @param sources clauses of import to be used.
* @param name relevant name ot the target element (e.g. of identifier in an experssion).
* @param editor target's editor.
*/
public ImportFromExistingAction(@NotNull PyElement target, @NotNull List<Pair<PyImportElement, PsiElement>> sources, String name, Editor editor) {
public ImportFromExistingAction(@NotNull PyElement target, @NotNull List<ImportCandidateHolder> sources, String name, Editor editor) {
mySources = sources;
myTarget = target;
myEditor = editor;
@@ -54,35 +55,31 @@ public class ImportFromExistingAction implements QuestionAction {
PsiDocumentManager.getInstance(myTarget.getProject()).commitAllDocuments();
if (!myTarget.isValid()) return false;
if ((myTarget instanceof PyQualifiedExpression) && ((((PyQualifiedExpression)myTarget).getQualifier() != null))) return false; // we cannot be qualified
for (Pair<PyImportElement, PsiElement> src : mySources) {
if (!src.getFirst().isValid()) return false;
if (!src.getSecond().isValid()) return false;
for (ImportCandidateHolder item : mySources) {
if (!item.getImportable().isValid()) return false;
if (!item.getFile().isValid()) return false;
if (item.getImportElement() != null && !item.getImportElement().isValid()) return false;
}
// act
if (mySources.size() > 1) {
selectSourceAndDo();
}
else doWriteAction(mySources.get(0).getFirst());
else doWriteAction(mySources.get(0));
return true;
}
private void selectSourceAndDo() {
// GUI part
QualifiedHolder[] items = new QualifiedHolder[mySources.size()];
int i = 0;
for (Pair<PyImportElement, PsiElement> pair : mySources) {
items[i] = new QualifiedHolder(pair.getFirst(), pair.getSecond(), myName);
i += 1;
}
ImportCandidateHolder[] items = mySources.toArray(new ImportCandidateHolder[mySources.size()]); // silly JList can't handle modern collections
final JList list = new JList(items);
list.setCellRenderer(new CellRenderer());
list.setCellRenderer(new CellRenderer(myName));
Runnable runnable = new Runnable() {
public void run() {
int index = list.getSelectedIndex();
if (index < 0) return;
PsiDocumentManager.getInstance(myTarget.getProject()).commitAllDocuments();
doWriteAction(mySources.get(index).getFirst());
doWriteAction(mySources.get(index));
}
};
@@ -94,34 +91,41 @@ public class ImportFromExistingAction implements QuestionAction {
;
}
private void doIt(final PyImportElement src) {
// did user choose 'import' or 'from import'?
PsiElement parent = src.getParent();
if (parent instanceof PyFromImportStatement) {
// add another import element right after the one we got
final PyElementGenerator gen = PythonLanguage.getInstance().getElementGenerator();
final Project project = myTarget.getProject();
PsiElement new_elt = gen.
createFromText(project, PyImportElement.class, "from foo import " + myName, new int[]{0,6})
;
PyUtil.addListNode(parent, new_elt, null, false, true);
private void doIt(final ImportCandidateHolder item) {
PyImportElement src = item.getImportElement();
final PyElementGenerator gen = PythonLanguage.getInstance().getElementGenerator();
if (src != null) { // use existing import
// did user choose 'import' or 'from import'?
PsiElement parent = src.getParent();
if (parent instanceof PyFromImportStatement) {
// add another import element right after the one we got
final Project project = myTarget.getProject();
PsiElement new_elt = gen.
createFromText(project, PyImportElement.class, "from foo import " + myName, new int[]{0, 6});
PyUtil.addListNode(parent, new_elt, null, false, true);
}
else { // just 'import'
// all we need is to qualify our target
myTarget.replace(gen.createExpressionFromText(myTarget.getProject(), src.getVisibleName() + "." + myName));
}
}
else { // just 'import'
// all we need is to qualify our target
myTarget.replace(
PythonLanguage.getInstance().
getElementGenerator().
createExpressionFromText(myTarget.getProject(), src.getVisibleName()+ "." + myName)
);
else { // no existing import, add it then use it
Project project = myTarget.getProject();
AddImportHelper.addImportStatement(myTarget.getContainingFile(), item.getPath(), item.getAsName(), project);
String qual_name;
if (item.getAsName() != null) qual_name = item.getAsName();
else qual_name = item.getPath();
myTarget.replace(gen.createExpressionFromText(project, qual_name + "." + myName));
}
}
private void doWriteAction(final PyImportElement src) {
private void doWriteAction(final ImportCandidateHolder item) {
PsiElement src = item.getImportable();
CommandProcessor.getInstance().executeCommand(src.getProject(), new Runnable() {
public void run() {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
public void run() {
doIt(src);
doIt(item);
}
});
}
@@ -129,64 +133,13 @@ public class ImportFromExistingAction implements QuestionAction {
}
// items to store in list
private static class QualifiedHolder {
final PyImportElement mySrc;
final PsiElement myItem;
final String myName;
public QualifiedHolder(PyImportElement src, PsiElement item, String name) {
mySrc = src;
myItem = item;
myName = name;
}
public Icon getIcon() {
return myItem.getIcon(0);
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
PsiElement parent = mySrc.getParent();
if (parent instanceof PyFromImportStatement) {
sb.append(myName);
}
else {
sb.append(mySrc.getVisibleName()).append(".").append(myName);
}
if (myItem instanceof PyFunction) {
sb.append("(");
// below: ", ".join([x.getRepr(False) for x in getParameters()])
PyParameter[] params = ((PyFunction)myItem).getParameterList().getParameters();
String[] param_reprs = new String[params.length];
for (int i=0; i < params.length; i += 1) param_reprs[i] = params[i].getRepr(false);
PyUtil.joinSubarray(param_reprs, 0, params.length, ", ", sb);
sb.append(")");
}
else if (myItem instanceof PyClass) {
PyClass[] supers = ((PyClass)myItem).getSuperClasses();
if (supers.length > 0) {
sb.append("(");
// ", ".join(x.getName() for x in getSuperClasses())
String[] super_names = new String[supers.length];
for (int i=0; i < supers.length; i += 1) super_names[i] = supers[i].getName();
PyUtil.joinSubarray(super_names, 0, supers.length, ", ", sb);
sb.append(")");
}
}
if (parent instanceof PyFromImportStatement) {
sb.append(" from ").append(((PyFromImportStatement)parent).getImportSource().getReferencedName());
}
return sb.toString();
}
}
// Stolen from FQNameCellRenderer
private static class CellRenderer extends SimpleColoredComponent implements ListCellRenderer {
private final Font FONT;
private final String myName;
public CellRenderer() {
public CellRenderer(String name) {
myName = name;
EditorColorsScheme scheme = EditorColorsManager.getInstance().getGlobalScheme();
FONT = new Font(scheme.getEditorFontName(), Font.PLAIN, scheme.getEditorFontSize());
setOpaque(true);
@@ -203,9 +156,9 @@ public class ImportFromExistingAction implements QuestionAction {
clear();
QualifiedHolder item = (QualifiedHolder)value;
setIcon(item.getIcon());
String item_name = item.toString();
ImportCandidateHolder item = (ImportCandidateHolder)value;
setIcon(item.getImportable().getIcon(0));
String item_name = item.getPresentableText(myName);
append(item_name, SimpleTextAttributes.REGULAR_ATTRIBUTES);
setFont(FONT);
if (isSelected) {
@@ -6,21 +6,23 @@ import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInspection.HintAction;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.util.IncorrectOperationException;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.psi.PyElement;
import com.jetbrains.python.psi.PyImportElement;
import com.jetbrains.python.psi.PyQualifiedExpression;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* Handles cases when an unresolved name may be imported from one of existing imported modules.
* The object contains a list of modules from which given name might be imported.
* The object contains a list of import candidates and serves only to show the initial hint;
* the actual work is done in ImportFromExistingAction..
* User: dcheryasov
* Date: Apr 15, 2009 2:06:25 PM
*/
@@ -28,36 +30,43 @@ public class ImportFromExistingFix implements HintAction {
PyElement myNode;
List<Pair<PyImportElement, PsiElement>> myImports; // from where and what to import
List<ImportCandidateHolder> myImports; // from where and what to import
String myName;
/**
* Creates a new, empty fix object.
* @param node to which the fix applies.
* @param name the unresolved identifier portion of node's text
*/
public ImportFromExistingFix(PyElement node, String name) {
myNode = node;
myImports = new ArrayList<Pair<PyImportElement, PsiElement>>();
myImports = new ArrayList<ImportCandidateHolder>();
myName = name;
}
/**
* Creates a mew fix object with one import variant.
* @param node to which the fix applies.
* @param source from which the name is importable.
* @param name
* Adds another import source.
* @param importable an element that could be imported either from import element or from file.
* @param file the file which is the source of the importable
* @param importElement an existing import element that can be a source for the importable.
*/
public ImportFromExistingFix(PyElement node, PyImportElement source, PsiElement item, String name) {
this(node, name);
addImport(source, item);
public void addImport(@NotNull PsiElement importable, @NotNull PsiFile file, @Nullable PyImportElement importElement) {
myImports.add(new ImportCandidateHolder(importable, file, importElement, null, null));
}
/**
* Adds another import source.
* @param source an import statement from which the name is importable.
* @param importable an element that could be imported either from import element or from file.
* @param file the file which is the source of the importable
* @param importElement an existing import element that can be a source for the importable.
* @param path import path for the file, as a qualified name (a.b.c)
* @param asName name to use to import the path as: "import path as asName"
*/
public void addImport(PyImportElement source, PsiElement item) {
myImports.add(new Pair<PyImportElement, PsiElement>(source, item));
public void addImport(
@NotNull PsiElement importable, @NotNull PsiFile file,
@Nullable PyImportElement importElement, @Nullable String path, @Nullable String asName
) {
myImports.add(new ImportCandidateHolder(importable, file, importElement, path, asName));
}
@NotNull
@@ -74,9 +83,10 @@ public class ImportFromExistingFix implements HintAction {
if (myNode == null || !myNode.isValid() || myNode.getName() == null || myImports.size() <= 0) {
return false; // TODO: also return false if an on-the-fly unambiguous fix is possible?
}
if ((myNode instanceof PyQualifiedExpression) && ((((PyQualifiedExpression)myNode).getQualifier() != null))) return false; // we cannot be qualified
final String message = ShowAutoImportPass.getMessage(
myImports.size() > 1,
myImports.get(0).getFirst().getVisibleName()+"."+myNode.getName()
ImportCandidateHolder.getQualifiedName(myName, myImports.get(0).getPath(), myImports.get(0).getImportElement())
);
final ImportFromExistingAction action = new ImportFromExistingAction(myNode, myImports, myName, editor);
HintManager.getInstance().showQuestionHint(
@@ -2,13 +2,16 @@ package com.jetbrains.python.inspections;
import com.intellij.codeHighlighting.HighlightDisplayLevel;
import com.intellij.codeInspection.*;
import com.intellij.lang.ASTNode;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiPolyVariantReference;
import com.intellij.psi.PsiReference;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.stubs.StubIndex;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.actions.AddFieldQuickFix;
import com.jetbrains.python.actions.AddImportAction;
import com.jetbrains.python.actions.AddMethodQuickFix;
@@ -17,6 +20,9 @@ import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyBuiltinCache;
import com.jetbrains.python.psi.resolve.CollectProcessor;
import com.jetbrains.python.psi.resolve.PyResolveUtil;
import com.jetbrains.python.psi.resolve.ResolveImportUtil;
import com.jetbrains.python.psi.stubs.PyClassNameIndex;
import com.jetbrains.python.psi.stubs.PyFunctionNameIndex;
import com.jetbrains.python.psi.types.PyClassType;
import com.jetbrains.python.psi.types.PyModuleType;
import com.jetbrains.python.psi.types.PyNoneType;
@@ -25,7 +31,7 @@ import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.*;
/**
* Marks references that fail to resolve.
@@ -76,6 +82,8 @@ public class PyUnresolvedReferencesInspection extends LocalInspectionTool {
static HintAction proposeImportFixes(final PyElement node, String ref_text) {
boolean worthy_fix = false;
ImportFromExistingFix fix = null;
Set<String> seen_file_names = new HashSet<String>(); // true import names
Set<String> seen_as_names = new HashSet<String>(); // 'as' parts: we don't want to clas with them either
// maybe the name is importable via some exisitng 'import foo' statement, and only needs a qualifier.
// walk up collecting all such statements and analyzing
CollectProcessor import_prc = new CollectProcessor(PyImportStatement.class);
@@ -88,10 +96,13 @@ public class PyUnresolvedReferencesInspection extends LocalInspectionTool {
final PyReferenceExpression src = ielt.getImportReference();
if (src != null) {
PsiElement dst = src.resolve();
if (dst instanceof PyFile) {
PsiElement res = ((PyFile)dst).findExportedName(ref_text);
if (dst instanceof PyFile) {
PyFile dst_file = (PyFile)dst;
seen_file_names.add(ielt.getImportReference().getReferencedName()); // ref is ok or matching would fail
seen_as_names.add(ielt.getVisibleName());
PsiElement res = (dst_file).findExportedName(ref_text);
if (res != null) {
fix.addImport(ielt, res);
fix.addImport(res, dst_file, ielt);
worthy_fix = true;
}
}
@@ -114,9 +125,11 @@ public class PyUnresolvedReferencesInspection extends LocalInspectionTool {
if (src != null) {
PsiElement dst = src.resolve();
if (dst instanceof PyFile) {
PsiElement res = ((PyFile)dst).findExportedName(ref_text);
PyFile dst_file = (PyFile)dst;
seen_file_names.add(from_stmt.getImportSource().getReferencedName()); // source is ok, else it won't match and we'd not be adding it
PsiElement res = (dst_file).findExportedName(ref_text);
if (res != null) {
fix.addImport(ielts[ielts.length-1], res); // last element; action expects to add to tail
fix.addImport(res, dst_file, ielts[ielts.length-1]); // last element; action expects to add to tail
worthy_fix = true;
}
}
@@ -124,10 +137,78 @@ public class PyUnresolvedReferencesInspection extends LocalInspectionTool {
}
}
}
// maybe some unimported file has it, too
// NOTE: current indices have limitations, only finding direct definitions of classes and functions.
Project project = node.getProject();
GlobalSearchScope scope = null; // GlobalSearchScope.projectScope(project);
List<PsiElement> symbols = new ArrayList<PsiElement>();
symbols.addAll(StubIndex.getInstance().get(PyClassNameIndex.KEY, ref_text, project, scope));
symbols.addAll(StubIndex.getInstance().get(PyFunctionNameIndex.KEY, ref_text, project, scope));
if (symbols.size() > 0) {
if (fix == null) fix = new ImportFromExistingFix(node, ref_text); // it might have been created in the previous scan, or not.
for (PsiElement symbol : symbols) {
if (symbol.getParent() instanceof PsiFile) { // we only want top-level symbols
PsiFile srcfile = symbol.getContainingFile();
if (srcfile != null) {
VirtualFile vfile = srcfile.getVirtualFile();
if (vfile != null) {
String import_path = ResolveImportUtil.findShortestImportableName(node, vfile);
if (import_path != null && !seen_file_names.contains(import_path)) {
// a new, valid hit
String as_name = null;
if (seen_as_names.contains(import_path)) {
// an 'as' name somewhere above eclipses the true name. get us a unique 'as' name.
as_name = propseAsName(node.getContainingFile(), import_path);
seen_as_names.add(as_name); // just in case
}
fix.addImport(symbol, srcfile, null, import_path, as_name);
seen_file_names.add(import_path); // just in case, again
}
}
}
}
}
}
if (worthy_fix) return fix;
else return null;
}
private final static String[] AS_PREFIXES = {"other_", "one_more_", "different_", "pseudo_", "true_"};
// a no-frills recursive accumulating scan
private static void collectIdentifiers(ASTNode node, Collection<String> dst) {
ASTNode seeker = node.getFirstChildNode();
while (seeker != null) {
if (seeker.getElementType() == PyTokenTypes.IDENTIFIER) dst.add(seeker.getText());
else collectIdentifiers(seeker, dst);
seeker = seeker.getTreeNext();
}
}
// find an unique name that does not clash with anything in the file, using ref_test and import_path as hints
private static String propseAsName(PsiFile file, String import_path) {
// a somehow brute-force approach: collect all identifiers wholesale and avoid clashes with any of them
Set<String> ident_set = new HashSet<String>();
collectIdentifiers(file.getNode(), ident_set);
// try flattened import path
String path_name = import_path.replace('.', '_');
if (! ident_set.contains(path_name)) return path_name;
// ...with prefixes: a highly improbable situation already
for (String prefix : AS_PREFIXES) {
String variant = prefix + path_name;
if (! ident_set.contains(variant)) return variant;
}
// if nothing helped, just bluntly add a number to the end. guaranteed to finish in ident_set.size() iterations.
int cnt = 1;
while (cnt < Integer.MAX_VALUE) {
String variant = path_name + Integer.toString(cnt);
if (! ident_set.contains(variant)) return variant;
cnt += 1;
}
return "SHOOSHPANCHICK"; // no, this cannot happen in a life-size file, just keeps inspections happy
}
@Override
public void visitPyElement(final PyElement node) {
super.visitPyElement(node); //To change body of overridden methods use File | Settings | File Templates.
@@ -27,6 +27,7 @@ import com.jetbrains.python.psi.PyElementVisitor;
import com.jetbrains.python.psi.PyFromImportStatement;
import com.jetbrains.python.psi.PyImportElement;
import com.jetbrains.python.psi.PyReferenceExpression;
import com.jetbrains.python.psi.resolve.ResolveImportUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -27,6 +27,7 @@ import com.jetbrains.python.PyElementTypes;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.resolve.PyAsScopeProcessor;
import com.jetbrains.python.psi.resolve.PyResolveUtil;
import com.jetbrains.python.psi.resolve.ResolveImportUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -2,6 +2,7 @@ package com.jetbrains.python.psi.impl;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.resolve.PyResolveUtil;
import com.jetbrains.python.psi.resolve.ResolveImportUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.lang.ASTNode;
@@ -1,4 +1,4 @@
package com.jetbrains.python.psi.impl;
package com.jetbrains.python.psi.resolve;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.DataContext;
@@ -13,12 +13,10 @@ import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.containers.HashSet;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.resolve.PyResolveUtil;
import com.jetbrains.python.psi.resolve.ResolveProcessor;
import com.jetbrains.python.psi.resolve.VariantsProcessor;
import com.jetbrains.python.psi.impl.PyImportResolver;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Iterator;
@@ -51,7 +49,7 @@ public class ResolveImportUtil {
* @return the object importRef refers to, or null.
*/
@Nullable
static PsiElement resolveImportReference(final PyReferenceExpression importRef) {
public static PsiElement resolveImportReference(final PyReferenceExpression importRef) {
if (importRef == null) return null; // fail fast
final String referencedName = importRef.getReferencedName();
if (referencedName == null) return null;
@@ -153,6 +151,66 @@ public class ResolveImportUtil {
return null; // not resolved by any means
}
public static void visitRoots(final PsiElement elt, @NotNull final SdkRootVisitor visitor) {
// real search
final Module module = ModuleUtil.findModuleForPsiElement(elt);
if (module != null) {
// TODO: implement a proper module-like approach in PyCharm for "project's dirs on pythonpath", minding proper search order
// Module-based approach works only in the IDEA plugin.
ModuleRootManager rootManager = ModuleRootManager.getInstance(module);
// look in module sources
boolean source_entries_missing = true;
for (ContentEntry entry: rootManager.getContentEntries()) {
VirtualFile root_file = entry.getFile();
if (!visitor.visitRoot(root_file)) return;
for (VirtualFile folder : entry.getSourceFolderFiles()) {
source_entries_missing = false;
if (!visitor.visitRoot(folder)) return;
}
}
if (source_entries_missing) {
// fallback for a case without any source entries: use project root
VirtualFile project_root = module.getProject().getBaseDir();
if (!visitor.visitRoot(project_root)) return;
}
// else look in SDK roots
RootPolicy<PsiElement> resolvePolicy = new RootPolicy<PsiElement>() {
@Nullable
public PsiElement visitJdkOrderEntry(final JdkOrderEntry jdkOrderEntry, final PsiElement value) {
if (value != null) return value; // for chaining in processOrder()
visitGivenRoots(jdkOrderEntry.getRootFiles(OrderRootType.SOURCES), visitor);
return null;
}
};
rootManager.processOrder(resolvePolicy, null);
}
else {
// no module, another way to look in SDK roots
final PsiFile elt_psifile = elt.getContainingFile();
if (elt_psifile != null) { // formality
final VirtualFile elt_vfile = elt_psifile.getVirtualFile();
if (elt_vfile != null) { // reality
for (OrderEntry entry: ProjectRootManager.getInstance(elt.getProject()).getFileIndex().getOrderEntriesForFile(elt_vfile
)
) {
if (!visitGivenRoots(entry.getFiles(OrderRootType.SOURCES), visitor)) break;
}
}
}
}
}
private static boolean visitGivenRoots(final VirtualFile[] roots, SdkRootVisitor visitor) {
for (VirtualFile root: roots) {
if (! visitor.visitRoot(root)) return false;
}
return true;
}
// TODO: rewrite using visitRoots
/**
* Looks for a name among element's module's roots; if there's no module, then among project's roots.
* @param elt PSI element that defines the module and/or the project.
@@ -206,7 +264,7 @@ public class ResolveImportUtil {
public PsiElement visitJdkOrderEntry(final JdkOrderEntry jdkOrderEntry, final PsiElement value) {
if (value != null) return value;
LookupRootVisitor visitor = new LookupRootVisitor(refName, elt.getManager());
visitRoots(jdkOrderEntry.getRootFiles(OrderRootType.SOURCES), visitor);
visitGivenRoots(jdkOrderEntry.getRootFiles(OrderRootType.SOURCES), visitor);
return visitor.getResult();
}
};
@@ -256,12 +314,6 @@ public class ResolveImportUtil {
return null;
}
public static void visitRoots(final VirtualFile[] roots, SdkRootVisitor visitor) {
for (VirtualFile root: roots) {
if (! visitor.visitRoot(root)) break;
}
}
/**
Tries to find referencedName under a root.
@param root where to look for the referenced name.
@@ -285,7 +337,6 @@ public class ResolveImportUtil {
return null;
}
interface SdkRootVisitor {
/**
* @param root what we're visiting.
@@ -486,7 +537,7 @@ public class ResolveImportUtil {
@Nullable
public PsiElement visitJdkOrderEntry(final JdkOrderEntry jdkOrderEntry, final PsiElement value) {
if (value != null) return value;
visitRoots(jdkOrderEntry.getRootFiles(OrderRootType.SOURCES), visitor);
visitGivenRoots(jdkOrderEntry.getRootFiles(OrderRootType.SOURCES), visitor);
return null;
}
};
@@ -497,4 +548,60 @@ public class ResolveImportUtil {
return variants.toArray(new Object[variants.size()]);
}
/**
* Tries to find roots that contain given vfile, and among them the root that contains at the smallest depth.
*/
private static class PathChoosingVisitor implements SdkRootVisitor {
private final VirtualFile myFile;
private String myFname;
private String myResult = null;
private int myDots = Integer.MAX_VALUE; // how many dots in the path
private PathChoosingVisitor(VirtualFile file) {
myFile = file;
// cut off the ext
myFname = file.getPath();
int pos = myFname.lastIndexOf('.');
if (pos > 0) myFname = myFname.substring(0, pos);
}
public boolean visitRoot(VirtualFile root) {
// does it ever fit?
String root_name = root.getPath()+"/";
if (myFname.startsWith(root_name)) {
String bet = myFname.substring(root_name.length()).replace('/', '.'); // "/usr/share/python/foo/bar" -> "foo.bar"
// count the dots
int dots = 0;
for (int i = 0; i < bet.length(); i += 1) if (bet.charAt(i) == '.') dots += 1;
// a better variant?
if (dots < myDots) {
myDots = dots;
myResult = bet;
}
}
return true; // visit all roots
}
public String getResult() {
return myResult;
}
}
/**
* Looks for a way to import given file.
* @param foothold an element in the file to import to (maybe the file itself); used to determine module, roots, etc.
* @param vfile file which importable name we want to find.
* @return a possibly qualified name under which the file may be imported, or null. If there's more than one way (overlapping roots),
* the name with fewest qualifiers is selected.
*/
@Nullable
public static String findShortestImportableName(PsiElement foothold, VirtualFile vfile) {
PathChoosingVisitor visitor = new PathChoosingVisitor(vfile);
visitRoots(foothold, visitor);
return visitor.getResult();
}
}
@@ -9,7 +9,7 @@ import com.jetbrains.python.psi.NameDefiner;
import com.jetbrains.python.psi.PyFile;
import com.jetbrains.python.psi.PyReferenceExpression;
import com.jetbrains.python.psi.PyUtil;
import com.jetbrains.python.psi.impl.ResolveImportUtil;
import com.jetbrains.python.psi.resolve.ResolveImportUtil;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
@@ -5,7 +5,7 @@ import com.intellij.psi.PsiFile;
import com.intellij.psi.ResolveState;
import com.jetbrains.python.psi.PyReferenceExpression;
import com.jetbrains.python.psi.resolve.VariantsProcessor;
import com.jetbrains.python.psi.impl.ResolveImportUtil;
import com.jetbrains.python.psi.resolve.ResolveImportUtil;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;