From 61463799aa94c50ef43e75347c2e8c30be14f776 Mon Sep 17 00:00:00 2001 From: Dmitry Cheryasov Date: Mon, 27 Apr 2009 08:33:14 +0400 Subject: [PATCH] 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. --- .../com/jetbrains/python/PyBundle.properties | 2 +- .../python/actions/AddImportAction.java | 46 ++---- .../python/actions/AddImportHelper.java | 66 +++++++++ .../python/actions/ImportCandidateHolder.java | 123 ++++++++++++++++ .../actions/ImportFromExistingAction.java | 139 ++++++------------ .../python/actions/ImportFromExistingFix.java | 40 +++-- .../PyUnresolvedReferencesInspection.java | 101 +++++++++++-- .../psi/impl/PyFromImportStatementImpl.java | 1 + .../python/psi/impl/PyImportElementImpl.java | 1 + .../psi/impl/PyStarImportElementImpl.java | 1 + .../{impl => resolve}/ResolveImportUtil.java | 137 +++++++++++++++-- .../python/psi/resolve/ResolveProcessor.java | 2 +- .../python/psi/types/PyModuleType.java | 2 +- 13 files changed, 488 insertions(+), 173 deletions(-) create mode 100644 python/src/com/jetbrains/python/actions/AddImportHelper.java create mode 100644 python/src/com/jetbrains/python/actions/ImportCandidateHolder.java rename python/src/com/jetbrains/python/psi/{impl => resolve}/ResolveImportUtil.java (80%) diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties index 37362f57571d..faebfe4169b7 100644 --- a/python/src/com/jetbrains/python/PyBundle.properties +++ b/python/src/com/jetbrains/python/PyBundle.properties @@ -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 \ No newline at end of file +ACT.qualify.with.module=Qualify with an imported module \ No newline at end of file diff --git a/python/src/com/jetbrains/python/actions/AddImportAction.java b/python/src/com/jetbrains/python/actions/AddImportAction.java index 76484897af3b..fd7e3d562159 100644 --- a/python/src/com/jetbrains/python/actions/AddImportAction.java +++ b/python/src/com/jetbrains/python/actions/AddImportAction.java @@ -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; } diff --git a/python/src/com/jetbrains/python/actions/AddImportHelper.java b/python/src/com/jetbrains/python/actions/AddImportHelper.java new file mode 100644 index 000000000000..2b7c6195416d --- /dev/null +++ b/python/src/com/jetbrains/python/actions/AddImportHelper.java @@ -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); + } + + } + +} diff --git a/python/src/com/jetbrains/python/actions/ImportCandidateHolder.java b/python/src/com/jetbrains/python/actions/ImportCandidateHolder.java new file mode 100644 index 000000000000..9ee0f582f99c --- /dev/null +++ b/python/src/com/jetbrains/python/actions/ImportCandidateHolder.java @@ -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(); + } +} diff --git a/python/src/com/jetbrains/python/actions/ImportFromExistingAction.java b/python/src/com/jetbrains/python/actions/ImportFromExistingAction.java index 364b8bd4d1df..0e5c4897992f 100644 --- a/python/src/com/jetbrains/python/actions/ImportFromExistingAction.java +++ b/python/src/com/jetbrains/python/actions/ImportFromExistingAction.java @@ -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> mySources; // list of + List mySources; // list of 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> sources, String name, Editor editor) { + public ImportFromExistingAction(@NotNull PyElement target, @NotNull List 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 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 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) { diff --git a/python/src/com/jetbrains/python/actions/ImportFromExistingFix.java b/python/src/com/jetbrains/python/actions/ImportFromExistingFix.java index 529a8073a2ca..f626f8a39762 100644 --- a/python/src/com/jetbrains/python/actions/ImportFromExistingFix.java +++ b/python/src/com/jetbrains/python/actions/ImportFromExistingFix.java @@ -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> myImports; // from where and what to import + List 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>(); + myImports = new ArrayList(); 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(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( diff --git a/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java b/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java index 877c1227a737..997847cf5158 100644 --- a/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java @@ -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 seen_file_names = new HashSet(); // true import names + Set seen_as_names = new HashSet(); // '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 symbols = new ArrayList(); + 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 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 ident_set = new HashSet(); + 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. diff --git a/python/src/com/jetbrains/python/psi/impl/PyFromImportStatementImpl.java b/python/src/com/jetbrains/python/psi/impl/PyFromImportStatementImpl.java index 83f89e0a2313..da59b53dd20c 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyFromImportStatementImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyFromImportStatementImpl.java @@ -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; diff --git a/python/src/com/jetbrains/python/psi/impl/PyImportElementImpl.java b/python/src/com/jetbrains/python/psi/impl/PyImportElementImpl.java index 2fe86b8e0861..d7afb3e6ef92 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyImportElementImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyImportElementImpl.java @@ -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; diff --git a/python/src/com/jetbrains/python/psi/impl/PyStarImportElementImpl.java b/python/src/com/jetbrains/python/psi/impl/PyStarImportElementImpl.java index a928e5ba5b6c..1387c1bbfbed 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyStarImportElementImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyStarImportElementImpl.java @@ -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; diff --git a/python/src/com/jetbrains/python/psi/impl/ResolveImportUtil.java b/python/src/com/jetbrains/python/psi/resolve/ResolveImportUtil.java similarity index 80% rename from python/src/com/jetbrains/python/psi/impl/ResolveImportUtil.java rename to python/src/com/jetbrains/python/psi/resolve/ResolveImportUtil.java index 5ffd27cd9d39..2fcae44617e8 100644 --- a/python/src/com/jetbrains/python/psi/impl/ResolveImportUtil.java +++ b/python/src/com/jetbrains/python/psi/resolve/ResolveImportUtil.java @@ -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 resolvePolicy = new RootPolicy() { + @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(); + } + + + } diff --git a/python/src/com/jetbrains/python/psi/resolve/ResolveProcessor.java b/python/src/com/jetbrains/python/psi/resolve/ResolveProcessor.java index 57cf90eb144d..f5579a57391a 100644 --- a/python/src/com/jetbrains/python/psi/resolve/ResolveProcessor.java +++ b/python/src/com/jetbrains/python/psi/resolve/ResolveProcessor.java @@ -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; diff --git a/python/src/com/jetbrains/python/psi/types/PyModuleType.java b/python/src/com/jetbrains/python/psi/types/PyModuleType.java index d2f17f548101..aa53d8ce423b 100644 --- a/python/src/com/jetbrains/python/psi/types/PyModuleType.java +++ b/python/src/com/jetbrains/python/psi/types/PyModuleType.java @@ -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;