From 11be0689a1e7787dcebae1ba3359a05086a24a88 Mon Sep 17 00:00:00 2001 From: Dmitry Cheryasov Date: Wed, 22 Apr 2009 06:15:45 +0400 Subject: [PATCH] Adds a kind of auto-import for names defined in already imported files. Hopefully fixes PY-119. Also refactors resolve processors and adds a bunch of small code improvements. --- .../python/psi/impl/PyJavaClassType.java | 4 +- .../com/jetbrains/python/PyBundle.properties | 8 +- .../python/actions/AddImportAction.java | 10 +- .../actions/ImportFromExistingAction.java | 222 ++++++ .../python/actions/ImportFromExistingFix.java | 104 +++ .../PyMethodParametersInspection.java | 1 + .../PyRedeclarationInspection.java | 4 +- .../PyUnresolvedReferencesInspection.java | 93 ++- .../src/com/jetbrains/python/psi/PyFile.java | 13 +- .../jetbrains/python/psi/PyImportElement.java | 5 + .../com/jetbrains/python/psi/PyParameter.java | 8 + .../jetbrains/python/psi/PyResolveUtil.java | 643 ------------------ .../src/com/jetbrains/python/psi/PyUtil.java | 31 +- ...Processor.java => PyAsScopeProcessor.java} | 2 +- .../python/psi/impl/PyClassImpl.java | 4 +- .../jetbrains/python/psi/impl/PyFileImpl.java | 10 + .../python/psi/impl/PyImportElementImpl.java | 16 +- .../python/psi/impl/PyParameterImpl.java | 13 + .../python/psi/impl/PyParameterListImpl.java | 16 +- .../psi/impl/PyReferenceExpressionImpl.java | 18 +- .../psi/impl/PyStarImportElementImpl.java | 1 + .../python/psi/impl/ResolveImportUtil.java | 16 +- .../resolve/AssignmentCollectProcessor.java | 83 +++ .../python/psi/resolve/CollectProcessor.java | 44 ++ .../psi/resolve/MultiResolveProcessor.java | 45 ++ .../psi/resolve/PyAsScopeProcessor.java | 11 + .../psi/resolve/PyClassScopeProcessor.java | 23 + .../python/psi/resolve/PyResolveUtil.java | 311 +++++++++ .../python/psi/resolve/ResolveProcessor.java | 112 +++ .../python/psi/resolve/VariantsProcessor.java | 110 +++ .../python/psi/types/PyClassType.java | 8 +- .../python/psi/types/PyModuleType.java | 6 +- .../python/validation/PyBuiltinAnnotator.java | 2 +- .../inspections/AddToImportFromFoo.py | 4 + .../inspections/AddToImportFromList.py | 3 + .../inspections/AddToImportFromList_after.py | 3 + .../testData/inspections/QualifyByImport.py | 3 + .../inspections/QualifyByImportFoo.py | 1 + .../com/jetbrains/python/QuickFixTest.java | 43 +- 39 files changed, 1338 insertions(+), 716 deletions(-) create mode 100644 python/src/com/jetbrains/python/actions/ImportFromExistingAction.java create mode 100644 python/src/com/jetbrains/python/actions/ImportFromExistingFix.java delete mode 100644 python/src/com/jetbrains/python/psi/PyResolveUtil.java rename python/src/com/jetbrains/python/psi/impl/{PyScopeProcessor.java => PyAsScopeProcessor.java} (76%) create mode 100644 python/src/com/jetbrains/python/psi/resolve/AssignmentCollectProcessor.java create mode 100644 python/src/com/jetbrains/python/psi/resolve/CollectProcessor.java create mode 100644 python/src/com/jetbrains/python/psi/resolve/MultiResolveProcessor.java create mode 100644 python/src/com/jetbrains/python/psi/resolve/PyAsScopeProcessor.java create mode 100644 python/src/com/jetbrains/python/psi/resolve/PyClassScopeProcessor.java create mode 100644 python/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java create mode 100644 python/src/com/jetbrains/python/psi/resolve/ResolveProcessor.java create mode 100644 python/src/com/jetbrains/python/psi/resolve/VariantsProcessor.java create mode 100644 python/testData/inspections/AddToImportFromFoo.py create mode 100644 python/testData/inspections/AddToImportFromList.py create mode 100644 python/testData/inspections/AddToImportFromList_after.py create mode 100644 python/testData/inspections/QualifyByImport.py create mode 100644 python/testData/inspections/QualifyByImportFoo.py diff --git a/python/pluginSrc/com/jetbrains/python/psi/impl/PyJavaClassType.java b/python/pluginSrc/com/jetbrains/python/psi/impl/PyJavaClassType.java index 8dd2ad4e648b..531a95137598 100644 --- a/python/pluginSrc/com/jetbrains/python/psi/impl/PyJavaClassType.java +++ b/python/pluginSrc/com/jetbrains/python/psi/impl/PyJavaClassType.java @@ -2,7 +2,7 @@ package com.jetbrains.python.psi.impl; import com.intellij.psi.*; import com.jetbrains.python.psi.PyReferenceExpression; -import com.jetbrains.python.psi.PyResolveUtil; +import com.jetbrains.python.psi.resolve.VariantsProcessor; import com.jetbrains.python.psi.types.PyType; /** @@ -26,7 +26,7 @@ public class PyJavaClassType implements PyType { } public Object[] getCompletionVariants(final PyReferenceExpression referenceExpression) { - final PyResolveUtil.VariantsProcessor processor = new PyResolveUtil.VariantsProcessor(); + final VariantsProcessor processor = new VariantsProcessor(); myClass.processDeclarations(processor, ResolveState.initial(), null, referenceExpression); return processor.getResult(); } diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties index 2e248555842a..37362f57571d 100644 --- a/python/src/com/jetbrains/python/PyBundle.properties +++ b/python/src/com/jetbrains/python/PyBundle.properties @@ -7,8 +7,11 @@ GNAME.item=item ### Actions: ACT ### ACT.FAMILY.import=import -# AddImport +# Actions and associated commands ACT.NAME.add.import=Add import +ACT.NAME.use.import=Find in imported modules + +ACT.CMD.use.import=Use an imported module ### Quick fixes ### QFIX.add.parameter.self=Add parameter 'self' @@ -133,4 +136,5 @@ runcfg.labels.interpreter=&Interpreter: 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 \ No newline at end of file +runcfg.captions.interpreter_options_dialog=Enter interpreter options +ACT.qualify.with.module=Qualify with 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 fde35b06305b..76484897af3b 100644 --- a/python/src/com/jetbrains/python/actions/AddImportAction.java +++ b/python/src/com/jetbrains/python/actions/AddImportAction.java @@ -15,13 +15,14 @@ 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.scope.PsiScopeProcessor; 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 org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -67,7 +68,7 @@ public class AddImportAction implements HintAction, QuestionAction, LocalQuickFi /** * Finds first import statement that imports given name. */ - private static class ImportLookupProcessor implements PsiScopeProcessor { + private static class ImportLookupProcessor implements PyClassScopeProcessor { String name; PsiElement found; @@ -101,6 +102,11 @@ public class AddImportAction implements HintAction, QuestionAction, LocalQuickFi public PsiElement getFound() { return found; } + + @NotNull + public Class[] getPossibleTargets() { + return NAME_DEFINER_ONLY; + } } /* diff --git a/python/src/com/jetbrains/python/actions/ImportFromExistingAction.java b/python/src/com/jetbrains/python/actions/ImportFromExistingAction.java new file mode 100644 index 000000000000..364b8bd4d1df --- /dev/null +++ b/python/src/com/jetbrains/python/actions/ImportFromExistingAction.java @@ -0,0 +1,222 @@ +package com.jetbrains.python.actions; + +import com.intellij.codeInsight.hint.QuestionAction; +import com.intellij.openapi.application.ApplicationManager; +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.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.intellij.ui.SimpleColoredComponent; +import com.intellij.ui.SimpleTextAttributes; +import com.jetbrains.python.PyBundle; +import com.jetbrains.python.PythonLanguage; +import com.jetbrains.python.psi.*; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; +import java.util.List; + +/** + * Turns an unqualified unresolved identifier into qualifed and resolvable. + * User: dcheryasov + * Date: Apr 15, 2009 6:24:48 PM + */ +public class ImportFromExistingAction implements QuestionAction { + + PyElement myTarget; + List> mySources; // list of + Editor myEditor; + String myName; + + /** + * @param target element to become qualified as imported. + * @param sources clauses of import to be used. + */ + public ImportFromExistingAction(@NotNull PyElement target, @NotNull List> sources, String name, Editor editor) { + mySources = sources; + myTarget = target; + myEditor = editor; + myName = name; + } + + /** + * Alters either target (by qualifying a name) or source (by explicitly importing the name). + * @return true if action succeeded + */ + public boolean execute() { + // check if the tree is sane + 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; + } + // act + if (mySources.size() > 1) { + selectSourceAndDo(); + } + else doWriteAction(mySources.get(0).getFirst()); + 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; + } + final JList list = new JList(items); + list.setCellRenderer(new CellRenderer()); + + 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()); + } + }; + + new PopupChooserBuilder(list). + setTitle(PyBundle.message("ACT.qualify.with.module")). + setItemChoosenCallback(runnable). + createPopup(). + showInBestPositionFor(myEditor) + ; + } + + 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); + } + else { // just 'import' + // all we need is to qualify our target + myTarget.replace( + PythonLanguage.getInstance(). + getElementGenerator(). + createExpressionFromText(myTarget.getProject(), src.getVisibleName()+ "." + myName) + ); + } + } + + private void doWriteAction(final PyImportElement src) { + CommandProcessor.getInstance().executeCommand(src.getProject(), new Runnable() { + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + public void run() { + doIt(src); + } + }); + } + }, PyBundle.message("ACT.CMD.use.import"), null); + } + + + // 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; + + public CellRenderer() { + EditorColorsScheme scheme = EditorColorsManager.getInstance().getGlobalScheme(); + FONT = new Font(scheme.getEditorFontName(), Font.PLAIN, scheme.getEditorFontSize()); + setOpaque(true); + } + + // value is a QualifiedHolder + public Component getListCellRendererComponent( + JList list, + Object value, // expected to be + int index, + boolean isSelected, + boolean cellHasFocus + ){ + + clear(); + + QualifiedHolder item = (QualifiedHolder)value; + setIcon(item.getIcon()); + String item_name = item.toString(); + append(item_name, SimpleTextAttributes.REGULAR_ATTRIBUTES); + setFont(FONT); + if (isSelected) { + setBackground(list.getSelectionBackground()); + setForeground(list.getSelectionForeground()); + } + else { + setBackground(list.getBackground()); + setForeground(list.getForeground()); + } + return this; + } + } +} diff --git a/python/src/com/jetbrains/python/actions/ImportFromExistingFix.java b/python/src/com/jetbrains/python/actions/ImportFromExistingFix.java new file mode 100644 index 000000000000..529a8073a2ca --- /dev/null +++ b/python/src/com/jetbrains/python/actions/ImportFromExistingFix.java @@ -0,0 +1,104 @@ +package com.jetbrains.python.actions; + +import com.intellij.codeInsight.CodeInsightUtilBase; +import com.intellij.codeInsight.daemon.impl.ShowAutoImportPass; +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 org.jetbrains.annotations.NotNull; + +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. + * User: dcheryasov + * Date: Apr 15, 2009 2:06:25 PM + */ +public class ImportFromExistingFix implements HintAction { + + PyElement myNode; + + List> myImports; // from where and what to import + String myName; + + /** + * Creates a new, empty fix object. + * @param node to which the fix applies. + */ + public ImportFromExistingFix(PyElement node, String name) { + myNode = node; + 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 + */ + public ImportFromExistingFix(PyElement node, PyImportElement source, PsiElement item, String name) { + this(node, name); + addImport(source, item); + } + + /** + * Adds another import source. + * @param source an import statement from which the name is importable. + */ + public void addImport(PyImportElement source, PsiElement item) { + myImports.add(new Pair(source, item)); + } + + @NotNull + public String getText() { + return PyBundle.message("ACT.NAME.use.import"); + } + + @NotNull + public String getFamilyName() { + return PyBundle.message("ACT.FAMILY.import"); + } + + public boolean showHint(Editor editor) { + 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? + } + final String message = ShowAutoImportPass.getMessage( + myImports.size() > 1, + myImports.get(0).getFirst().getVisibleName()+"."+myNode.getName() + ); + final ImportFromExistingAction action = new ImportFromExistingAction(myNode, myImports, myName, editor); + HintManager.getInstance().showQuestionHint( + editor, message, + myNode.getTextOffset(), + myNode.getTextRange().getEndOffset(), action); + return true; + } + + public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { + return myNode != null && myNode.isValid() && myImports.size() > 0; + } + + public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { + // make sure file is committed, writable, etc + if (!CodeInsightUtilBase.prepareFileForWrite(file)) return; + // act + ImportFromExistingAction action = new ImportFromExistingAction(myNode, myImports, myName, editor); + action.execute(); // assume that action runs in WriteAction on its own behalf + } + + public boolean startInWriteAction() { + return false; // multiple variants may make us show a menu + } +} diff --git a/python/src/com/jetbrains/python/inspections/PyMethodParametersInspection.java b/python/src/com/jetbrains/python/inspections/PyMethodParametersInspection.java index 3e9ac851f739..60df0c0ccf65 100644 --- a/python/src/com/jetbrains/python/inspections/PyMethodParametersInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyMethodParametersInspection.java @@ -11,6 +11,7 @@ import com.jetbrains.python.actions.AddSelfQuickFix; import com.jetbrains.python.actions.RenameToSelfQuickFix; import com.jetbrains.python.psi.*; import com.jetbrains.python.psi.PyDecorator; +import com.jetbrains.python.psi.resolve.PyResolveUtil; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; diff --git a/python/src/com/jetbrains/python/inspections/PyRedeclarationInspection.java b/python/src/com/jetbrains/python/inspections/PyRedeclarationInspection.java index 6668fd17c838..91456cfe8e86 100644 --- a/python/src/com/jetbrains/python/inspections/PyRedeclarationInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyRedeclarationInspection.java @@ -8,6 +8,8 @@ import com.intellij.psi.PsiElementVisitor; import com.jetbrains.python.PyTokenTypes; import com.jetbrains.python.PyBundle; import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.resolve.PyResolveUtil; +import com.jetbrains.python.psi.resolve.ResolveProcessor; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; @@ -68,7 +70,7 @@ public class PyRedeclarationInspection extends LocalInspectionTool { private void _checkAbove(PyElement node, String kind) { String name = node.getName(); if (name != null) { - PyResolveUtil.ResolveProcessor proc = new PyResolveUtil.ResolveProcessor(node.getName()); + ResolveProcessor proc = new ResolveProcessor(node.getName()); PyResolveUtil.treeCrawlUp(proc, node); PsiElement found = proc.getResult(); // TODO: check if the redefined name is used somehow diff --git a/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java b/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java index 2955a9ff782a..877c1227a737 100644 --- a/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java @@ -1,10 +1,7 @@ package com.jetbrains.python.inspections; import com.intellij.codeHighlighting.HighlightDisplayLevel; -import com.intellij.codeInspection.LocalInspectionTool; -import com.intellij.codeInspection.LocalQuickFix; -import com.intellij.codeInspection.ProblemHighlightType; -import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.codeInspection.*; import com.intellij.lang.annotation.HighlightSeverity; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; @@ -15,14 +12,20 @@ import com.jetbrains.python.PyBundle; import com.jetbrains.python.actions.AddFieldQuickFix; import com.jetbrains.python.actions.AddImportAction; import com.jetbrains.python.actions.AddMethodQuickFix; +import com.jetbrains.python.actions.ImportFromExistingFix; 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.types.PyClassType; import com.jetbrains.python.psi.types.PyModuleType; import com.jetbrains.python.psi.types.PyNoneType; import com.jetbrains.python.psi.types.PyType; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; /** * Marks references that fail to resolve. @@ -69,6 +72,62 @@ public class PyUnresolvedReferencesInspection extends LocalInspectionTool { super(holder); } + @Nullable + static HintAction proposeImportFixes(final PyElement node, String ref_text) { + boolean worthy_fix = false; + ImportFromExistingFix fix = null; + // 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); + PyResolveUtil.treeCrawlUp(import_prc, node); + List result = import_prc.getResult(); + if (result.size() > 0) { + fix = new ImportFromExistingFix(node, ref_text); // initially it is almost as lightweight as a plain list + for (PsiElement stmt : import_prc.getResult()) { + for (PyImportElement ielt : ((PyImportStatement)stmt).getImportElements()) { + final PyReferenceExpression src = ielt.getImportReference(); + if (src != null) { + PsiElement dst = src.resolve(); + if (dst instanceof PyFile) { + PsiElement res = ((PyFile)dst).findExportedName(ref_text); + if (res != null) { + fix.addImport(ielt, res); + worthy_fix = true; + } + } + } + } + } + } + // maybe the name is importable via some exisitng 'from foo import ...' statement, and only needs another name to be imported. + // walk up collecting all such statements and analyzing + CollectProcessor from_import_prc = new CollectProcessor(PyFromImportStatement.class); + PyResolveUtil.treeCrawlUp(from_import_prc, node); + result = from_import_prc.getResult(); + if (result.size() > 0) { + if (fix == null) fix = new ImportFromExistingFix(node, ref_text); // it might have been created in the previous scan, or not. + for (PsiElement stmt : from_import_prc.getResult()) { + PyFromImportStatement from_stmt = (PyFromImportStatement)stmt; + PyImportElement[] ielts = from_stmt.getImportElements(); + if (ielts != null && ielts.length > 0) { + final PyReferenceExpression src = from_stmt.getImportSource(); + if (src != null) { + PsiElement dst = src.resolve(); + if (dst instanceof PyFile) { + PsiElement res = ((PyFile)dst).findExportedName(ref_text); + if (res != null) { + fix.addImport(ielts[ielts.length-1], res); // last element; action expects to add to tail + worthy_fix = true; + } + } + } + } + } + } + if (worthy_fix) return fix; + else return null; + } + @Override public void visitPyElement(final PyElement node) { super.visitPyElement(node); //To change body of overridden methods use File | Settings | File Templates. @@ -88,10 +147,11 @@ public class PyUnresolvedReferencesInspection extends LocalInspectionTool { unresolved = (reference.resolve() == null); } if (unresolved) { - StringBuffer description_buf = new StringBuffer(""); + StringBuffer description_buf = new StringBuffer(""); // TODO: clear description_buf logic. maybe a flag is needed instead. String text = reference.getElement().getText(); String ref_text = reference.getRangeInElement().substring(text); // text of the part we're working with LocalQuickFix action = null; + HintAction hint_action = null; if (ref_text.length() <= 0) return; // empty text, nothing to highlight if (reference instanceof PyReferenceExpression) { PyReferenceExpression refex = (PyReferenceExpression)reference; @@ -102,15 +162,20 @@ public class PyUnresolvedReferencesInspection extends LocalInspectionTool { } // unqualified: // may be module's - if ((new PyModuleType(node.getContainingFile())).getPossibleInstanceMembers().contains(refname)) continue; - // may be try: import; not an error not to resolve - if ((PsiTreeUtil.getParentOfType( - PsiTreeUtil.getParentOfType(node, PyImportElement.class), PyTryExceptStatement.class, PyIfStatement.class) != null) - ) { + if (PyModuleType.getPossibleInstanceMembers().contains(refname)) continue; + // may be a "try: import ..."; not an error not to resolve + if (( + PsiTreeUtil.getParentOfType( + PsiTreeUtil.getParentOfType(node, PyImportElement.class), PyTryExceptStatement.class, PyIfStatement.class + ) != null + )) { severity = HighlightSeverity.INFO; String errmsg = PyBundle.message("INSP.module.$0.not.found", ref_text); description_buf.append(errmsg); + // TODO: mark the node so that future references pointing to it won't result in a error, but in a warning } + // look in other imported modules for this whole name + hint_action = proposeImportFixes(node, ref_text); } if (reference instanceof PsiReferenceEx) { final String s = ((PsiReferenceEx)reference).getUnresolvedDescription(); @@ -127,11 +192,7 @@ public class PyUnresolvedReferencesInspection extends LocalInspectionTool { // this almost always means that we don't know the type, so don't show an error in this case continue; } - /* - PyReferenceExpression qref = (PyReferenceExpression)qexpr; - PsiElement qual_resolved = qref.resolve(); - */ - if (/*qual_resolved instanceof PyClass*/ qtype != null && qtype instanceof PyClassType) { + if (qtype != null && qtype instanceof PyClassType) { PyClass cls = ((PyClassType)qtype).getPyClass(); if (cls != null) { if (reference.getElement().getParent() instanceof PyCallExpression) { @@ -160,7 +221,7 @@ public class PyUnresolvedReferencesInspection extends LocalInspectionTool { } PsiElement point = node.getLastChild(); // usually the identifier at the end of qual ref if (point == null) point = node; - registerProblem(/*reference.getElement()*/ point, description, hl_type, null, action); + registerProblem(point, description, hl_type, hint_action, action); } } } diff --git a/python/src/com/jetbrains/python/psi/PyFile.java b/python/src/com/jetbrains/python/psi/PyFile.java index e039099c8b32..9466f409f094 100644 --- a/python/src/com/jetbrains/python/psi/PyFile.java +++ b/python/src/com/jetbrains/python/psi/PyFile.java @@ -4,8 +4,10 @@ package com.jetbrains.python.psi; import com.intellij.openapi.util.Key; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; @@ -20,7 +22,16 @@ public interface PyFile extends PyElement, PsiFile { List getTopLevelFunctions(); List getTopLevelAttributes(); - + + /** + * Looks for a name exported by this file, preferably in an efficient way. + * @param name what to find + * @param project project to use (useful for out-of-hierarchy files) + * @return found element, or null. + */ + @Nullable + PsiElement findExportedName(String name); + /** @return an URL of file, maybe bogus if virtual file is not present. */ diff --git a/python/src/com/jetbrains/python/psi/PyImportElement.java b/python/src/com/jetbrains/python/psi/PyImportElement.java index 01d906d749e2..dacd82730958 100644 --- a/python/src/com/jetbrains/python/psi/PyImportElement.java +++ b/python/src/com/jetbrains/python/psi/PyImportElement.java @@ -33,5 +33,10 @@ public interface PyImportElement extends PyElement, NameDefiner { @Nullable PyTargetExpression getAsName(); + /** + * @return name under which the element is wisible, that is, "as name" is there is one, or just name. + */ + @Nullable + String getVisibleName(); } diff --git a/python/src/com/jetbrains/python/psi/PyParameter.java b/python/src/com/jetbrains/python/psi/PyParameter.java index ca7a16fb5394..3d0f44fa4a83 100644 --- a/python/src/com/jetbrains/python/psi/PyParameter.java +++ b/python/src/com/jetbrains/python/psi/PyParameter.java @@ -19,6 +19,7 @@ package com.jetbrains.python.psi; import com.intellij.psi.PsiNamedElement; import com.intellij.psi.StubBasedPsiElement; import com.jetbrains.python.psi.stubs.PyParameterStub; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** @@ -35,5 +36,12 @@ public interface PyParameter extends PyElement, PsiNamedElement, PyExpression, S @Nullable PyExpression getDefaultValue(); + + /** + * @param includeDefaultValue if true, include the default value after an " = ". + * @return Canonical representation of parameter. Includes asterisks for *param and **param, and name. + */ + @NotNull + String getRepr(boolean includeDefaultValue); } diff --git a/python/src/com/jetbrains/python/psi/PyResolveUtil.java b/python/src/com/jetbrains/python/psi/PyResolveUtil.java deleted file mode 100644 index 74c7086c4978..000000000000 --- a/python/src/com/jetbrains/python/psi/PyResolveUtil.java +++ /dev/null @@ -1,643 +0,0 @@ -/* - * Copyright 2005 Pythonid Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS"; BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.jetbrains.python.psi; - -import com.intellij.codeInsight.lookup.LookupElement; -import com.intellij.codeInsight.lookup.LookupElementFactory; -import com.intellij.codeInsight.lookup.LookupItem; -import com.intellij.openapi.progress.ProgressManager; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.*; -import com.intellij.psi.scope.PsiScopeProcessor; -import com.intellij.psi.util.PsiTreeUtil; -import com.jetbrains.python.psi.impl.PyScopeProcessor; -import com.jetbrains.python.psi.impl.ResolveImportUtil; -import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.*; - -/** - * Ref resolution routines. - * User: yole - * Date: 14.06.2005 - */ -public class PyResolveUtil { - - private PyResolveUtil() { - } - - - /** - * Tries to find nearest parent that conceals nams definded inside it. Such elements are 'class' and 'def': - * anything defined within it does not seep to the namespace below them, but is concealed within. - * @param elt starting point of search. - * @return 'class' or 'def' element, or null if not found. - */ - @Nullable - public static PsiElement getConcealingParent(PsiElement elt) { - return PsiTreeUtil.getParentOfType(elt, PyClass.class, PyFunction.class); - } - - protected static PsiElement getInnermostChildOf(PsiElement elt) { - PsiElement feeler = elt; - PsiElement seeker; - seeker = feeler; - // find innermost last child of the subtree we're in - while (feeler != null) { - seeker = feeler; - feeler = feeler.getLastChild(); - } - return seeker; - } - - /** - * Returns closest previous node of given class, as input file would have it. - * @param elt node from which to look for a previous atatement. - * @param cls class of the previous node to find. - * @return previous statement, or null. - */ - @Nullable - public static T getPrevNodeOf(PsiElement elt, Class cls) { - PsiElement seeker = elt; - while (seeker != null) { - PsiElement feeler = seeker.getPrevSibling(); - if (feeler != null) { - seeker = getInnermostChildOf(feeler); - } - else { // we were the first subnode - // find something above the parent node we've not exhausted yet - seeker = seeker.getParent(); - if (seeker instanceof PyFile) return null; // all file nodes have been looked up, in vain - } - if (cls.isInstance(seeker)) return (T)seeker; - } - // here elt is null or a PsiFile is not up in the parent chain. - return null; - } - - /** - * Crawls up the PSI tree, checking nodes as if crawling backwards through source lexemes. - * @param processor a visitor that says when the crawl is done and collects info. - * @param elt element from which we start (not checked by processor); if null, the search immediately returns null. - * @param roof if not null, search continues only below the roof and including it. - * @param fromunder if true, begin search not above elt, but from a [possibly imaginary] node right below elt; so elt gets analyzed, too. - * @return first element that the processor accepted. - */ - @Nullable - public static PsiElement treeCrawlUp(PsiScopeProcessor processor, boolean fromunder, PsiElement elt, PsiElement roof) { - if (elt == null) return null; // can't find anyway. - PsiElement seeker = elt; - PsiElement cap = getConcealingParent(elt); - do { - ProgressManager.getInstance().checkCanceled(); - if (fromunder) { - fromunder = false; // only honour fromunder once per call - seeker = getPrevNodeOf(getInnermostChildOf(seeker), NameDefiner.class); - } - else { // main case - seeker = getPrevNodeOf(seeker, NameDefiner.class); - } - // aren't we in the same defining assignment, global, etc? - if ((seeker != null) && ((NameDefiner)seeker).mustResolveOutside() && PsiTreeUtil.isAncestor(seeker, elt, true) - ) { - seeker = getPrevNodeOf(seeker, NameDefiner.class); - } - // maybe we're under a cap? - while (true) { - PsiElement local_cap = getConcealingParent(seeker); - if (local_cap == null) break; // seeker is in global context - if (local_cap == cap) break; // seeker is in the same context as elt - if ((cap != null) && PsiTreeUtil.isAncestor(local_cap, cap, true)) break; // seeker is in a context above elt's - if ( - (local_cap != elt) && // elt isn't the cap of seeker itself - ((cap == null) || !PsiTreeUtil.isAncestor(local_cap, cap, true)) // elt's cap is not under local cap - ) { // only look at local cap and above - if (local_cap instanceof NameDefiner) seeker = local_cap; - else seeker = getPrevNodeOf(local_cap, NameDefiner.class); - } - else break; // seeker is contextually under elt already - } - // are we still under the roof? - if ((roof != null) && (seeker != null) && ! PsiTreeUtil.isAncestor(roof, seeker, false)) return null; - // maybe we're capped by a class? - if (refersFromMethodToClass(cap, seeker)) continue; - // check what we got - if (seeker != null) { - if (!processor.execute(seeker, ResolveState.initial())) { - if (processor instanceof ResolveProcessor) { - return ((ResolveProcessor)processor).getResult(); - } - else return seeker; // can't point to exact element, but somewhere here - } - } - } while (seeker != null); - return null; - } - - @Nullable - public static PsiElement resolveOffContext(@NotNull PyReferenceExpression refex) { - // if we're under a cap, an external object that we want to use might be also defined below us. - // look through all contexts, closest first. - PsiElement ret = null; - PsiElement our_cap = getConcealingParent(refex); - ResolveProcessor proc = new ResolveProcessor(refex.getReferencedName()); // processor reusable till first hit - if (our_cap != null) { - PsiElement cap = our_cap; - while (true) { - cap = getConcealingParent(cap); - if (cap == null) cap = refex.getContainingFile(); - ret = treeCrawlUp(proc, true, cap); - if ((ret != null) && !PsiTreeUtil.isAncestor(our_cap, ret, true)) { // found something and it is below our cap - // maybe we're in a method, and what we found is in its class context? - if (! refersFromMethodToClass(our_cap, ret)) { - break; // not in method -> must be all right - } - } - if (cap instanceof PsiFile) break; // file level, can't try more - } - } - return ret; - } - - /** - * @param inner an element presumably inside a method within a class, or a method itself. - * @param outer an element presumably in the class context. - * @return true if an outer element is in a class context, while the inner is a method or function inside it. - * @see com.jetbrains.python.psi.PyResolveUtil#getConcealingParent(com.intellij.psi.PsiElement) - */ - protected static boolean refersFromMethodToClass(final PsiElement inner, final PsiElement outer) { - return ( - (getConcealingParent(outer) instanceof PyClass) && // outer is in a class context - (PsiTreeUtil.getParentOfType(inner, PyFunction.class, false) != null) // inner is a function or method within the class - ); - } - - /** - * Crawls up the PSI tree, checking nodes as if crawling backwards through source lexemes. - * @param processor a visitor that says when the crawl is done and collects info. - * @param fromunder if true, search not above elt, but from a [possibly imaginary] node right below elt; so elt gets analyzed, too. - * @param elt element from which we start (not checked by processor); if null, the search immediately fails. - * @return first element that the processor accepted. - */ - @Nullable - public static PsiElement treeCrawlUp(PsiScopeProcessor processor, boolean fromunder, PsiElement elt) { - return treeCrawlUp(processor, fromunder, elt, null); - } - - /** - * Returns treeCrawlUp(processor, elt, false). A convenience method. - * @see PyResolveUtil#treeCrawlUp(com.intellij.psi.scope.PsiScopeProcessor,boolean,com.intellij.psi.PsiElement) - */ - @Nullable - public static PsiElement treeCrawlUp(PsiScopeProcessor processor, PsiElement elt) { - return treeCrawlUp(processor, false, elt); - } - - - /** - * Tries to match two [qualified] reference expression paths by names; target must be a 'sublist' of source to match. - * E.g., 'a.b.c.d' and 'a.b.c' would match, while 'a.b.c' and 'a.b.c.d' would not. Eqaully, 'a.b.c' and 'a.b.d' would not match. - * If either source or target is null, false is returned. - * @see #unwindQualifiers(PyQualifiedExpression) . - * @param source_path expression path to match (the longer list of qualifiers). - * @param target_path expression path to match against (hopeful sublist of qualifiers of source). - * @return true if source matches target. - */ - public static boolean pathsMatch(List source_path, List target_path) { - // turn qualifiers into lists - if ((source_path == null) || (target_path == null)) return false; - // compare until target is exhausted - Iterator source_iter = source_path.iterator(); - for (final T target_elt : target_path) { - if (source_iter.hasNext()) { - S source_elt = source_iter.next(); - if (!target_elt.getText().equals(source_elt.getText())) return false; - } - else return false; // source exhausted before target - } - return true; - } - - /** - * Unwinds a [multi-level] qualified expression into a path, as seen in source text, i.e. outermost qualifier first. - * If any qualifier happens to be not a PyQualifiedExpression, or expr is null, null is returned. - * @param expr an experssion to unwind. - * @return path as a list of ref expressions, or null. - */ - @Nullable - public static List unwindQualifiers(final T expr) { - final List path = new LinkedList(); - PyExpression maybe_step; - T step = expr; - try { - while (step != null) { - path.add(0, step); - maybe_step = step.getQualifier(); - step = (T)maybe_step; - } - } - catch (ClassCastException e) { - return null; - } - return path; - } - - public static String toPath(PyQualifiedExpression expr, String separator) { - if (expr == null) return ""; - List path = unwindQualifiers(expr); - if (path != null) { - StringBuilder buf = new StringBuilder(); - boolean is_not_first = false; - for (PyQualifiedExpression ex : path) { - if (is_not_first) buf.append(separator); - else is_not_first = true; - buf.append(ex.getName()); - } - return buf.toString(); - } - else return expr.getName(); - } - - public static class CollectProcessor implements PsiScopeProcessor { - - Class[] my_collectables; - List my_result; - - public CollectProcessor(Class... collectables) { - my_collectables = collectables; - my_result = new ArrayList(); - } - - public boolean execute(final PsiElement element, final ResolveState state) { - for (Class cls : my_collectables) { - if (cls.isInstance(element)) { - my_result.add((T)element); - } - } - return false; - } - - public T getHint(final Class hintClass) { - return null; - } - - public void handleEvent(final Event event, final Object associated) { - } - - public List getResult() { - return my_result; - } - } - - public static class ResolveProcessor implements PyScopeProcessor { - private final String myName; - private PsiElement myResult = null; - /*private Set mySeen;*/ - private final List myDefiners; - - public ResolveProcessor(final String name) { - myName = name; - /*mySeen = new HashSet();*/ - myDefiners = new ArrayList(2); // 1 is typical, 2 is sometimes, more is rare. - } - - public PsiElement getResult() { - return myResult; - } - - @NonNls - static String _nvl(Object s) { - if (s != null) return "'" + s.toString() + "'"; - else return "null"; - } - - /* - public Set getSeen() { - return mySeen; - } - */ - - /** - * Adds a NameDefiner point which is a secondary resolution target. E.g. import statement for imported name. - * @param definer - */ - protected void addNameDefiner(NameDefiner definer) { - myDefiners.add(definer); - } - - public ListgetDefiners() { - return myDefiners; - } - - public String toString() { - return _nvl(myName) + ", " + _nvl(myResult); - } - - public boolean execute(PsiElement element, ResolveState substitutor) { - if (element instanceof PyFile) { - final VirtualFile file = ((PyFile)element).getVirtualFile(); - if (file != null) { - if (myName.equals(file.getNameWithoutExtension())) { - myResult = element; - return false; - } - else if (ResolveImportUtil.INIT_PY.equals(file.getName())) { - VirtualFile dir = file.getParent(); - if ((dir != null) && myName.equals(dir.getName())) { - myResult = element; - return false; - } - } - } - } - else if (element instanceof PsiNamedElement) { - if (myName.equals(((PsiNamedElement)element).getName())) { - myResult = element; - return false; - } - } - else if (element instanceof PyReferenceExpression) { - PyReferenceExpression expr = (PyReferenceExpression)element; - String referencedName = expr.getReferencedName(); - if (referencedName != null && referencedName.equals(myName)) { - myResult = element; - return false; - } - } - else if (element instanceof NameDefiner) { - final NameDefiner definer = (NameDefiner)element; - PsiElement by_name = definer.getElementNamed(myName); - if (by_name != null) { - myResult = by_name; - if (!PsiTreeUtil.isAncestor(element, by_name, true)) { // non-trivial definer - addNameDefiner(definer); - } - return false; - } - } - - return true; - } - - public boolean execute(final PsiElement element, final String asName) { - if (asName.equals(myName)) { - myResult = element; - return false; - } - return true; - } - - @Nullable - public T getHint(Class hintClass) { - return null; - } - - public void handleEvent(Event event, Object associated) { - } - - - } - - public static class MultiResolveProcessor implements PsiScopeProcessor { - private final String _name; - private final List _results = new ArrayList(); - - public MultiResolveProcessor(String name) { - _name = name; - } - - public ResolveResult[] getResults() { - return _results.toArray(new ResolveResult[_results.size()]); - } - - public boolean execute(PsiElement element, ResolveState substitutor) { - if (element instanceof PsiNamedElement) { - if (_name.equals(((PsiNamedElement)element).getName())) { - _results.add(new PsiElementResolveResult(element)); - } - } - else if (element instanceof PyReferenceExpression) { - PyReferenceExpression expr = (PyReferenceExpression)element; - String referencedName = expr.getReferencedName(); - if (referencedName != null && referencedName.equals(_name)) { - _results.add(new PsiElementResolveResult(element)); - } - } - - return true; - } - - public T getHint(Class hintClass) { - return null; - } - - public void handleEvent(Event event, Object associated) { - } - } - - public static class VariantsProcessor implements PsiScopeProcessor { - private final Map myVariants = new HashMap(); - - protected String my_notice; - - public VariantsProcessor() { - // empty - } - - public VariantsProcessor(final Filter filter) { - my_filter = filter; - } - - protected Filter my_filter; - - public void setNotice(@Nullable String notice) { - my_notice = notice; - } - - protected void setupItem(LookupItem item) { - if (my_notice != null) { - setItemNotice(item, my_notice); - } - } - - protected static void setItemNotice(final LookupItem item, String notice) { - item.setAttribute(item.TAIL_TEXT_ATTR, notice); - item.setAttribute(item.TAIL_TEXT_SMALL_ATTR, ""); - } - - public LookupElement[] getResult() { - final Collection variants = myVariants.values(); - return variants.toArray(new LookupElement[variants.size()]); - } - - public List getResultList() { - return new ArrayList(myVariants.values()); - } - - public boolean execute(PsiElement element, ResolveState substitutor) { - if (my_filter != null && !my_filter.accept(element)) return true; // skip whatever the filter rejects - // TODO: refactor to look saner; much code duplication - if (element instanceof PsiNamedElement) { - final PsiNamedElement psiNamedElement = (PsiNamedElement)element; - final String name = psiNamedElement.getName(); - if (!myVariants.containsKey(name)) { - final LookupItem lookup_item = (LookupItem)LookupElementFactory.getInstance().createLookupElement(psiNamedElement); - setupItem(lookup_item); - myVariants.put(name, lookup_item); - } - } - else if (element instanceof PyReferenceExpression) { - PyReferenceExpression expr = (PyReferenceExpression)element; - String referencedName = expr.getReferencedName(); - if (referencedName != null && !myVariants.containsKey(referencedName)) { - final LookupItem lookup_item = (LookupItem)LookupElementFactory.getInstance().createLookupElement(referencedName); - setupItem(lookup_item); - myVariants.put(referencedName, lookup_item); - } - } - else if (element instanceof NameDefiner) { - final NameDefiner definer = (NameDefiner)element; - for (PyElement expr: definer.iterateNames()) { - if (expr != null) { // NOTE: maybe rather have SingleIterables skip nulls outright? - String referencedName = expr.getName(); - if (referencedName != null && !myVariants.containsKey(referencedName)) { - final LookupItem lookup_item = (LookupItem)LookupElementFactory.getInstance().createLookupElement(referencedName); - setupItem(lookup_item); - if (definer instanceof PyImportElement) { // set notice to imported module name if needed - PsiElement maybe_from_import = definer.getParent(); - if (maybe_from_import instanceof PyFromImportStatement) { - final PyFromImportStatement from_import = (PyFromImportStatement)maybe_from_import; - PyReferenceExpression src = from_import.getImportSource(); - if (src != null) { - setItemNotice(lookup_item, " | " + src.getName()); - } - } - } - myVariants.put(referencedName, lookup_item); - } - } - } - } - - return true; - } - - @Nullable - public T getHint(Class hintClass) { - return null; - } - - public void handleEvent(Event event, Object associated) { - } - } - - /** - * A simple interface allowing to filter processor results. - */ - public interface Filter { - /** - * @param target the object a processor is currently looking at. - * @return true if the object is acceptable as a processor result. - */ - boolean accept(Object target); - } - - public static class FilterNotInstance implements Filter { - Object instance; - - public FilterNotInstance(Object instance) { - this.instance = instance; - } - - public boolean accept(final Object target) { - return (instance != target); - } - } - - /** - * Collects all assignments in context above given element, if they match given naming pattern. - * Used to track creation of attributes by assignment (e.g in constructor). - */ - public static class AssignmentCollectProcessor implements PsiScopeProcessor { - - List my_qualifier; - List my_result; - Set my_seen_names; - - /** - * Creates an instance to collect assignments of attributes to the object identified by 'qualifier'. - * E.g. if qualifier = {"foo", "bar"} then assignments like "foo.bar.baz = ..." will be considered. - * The collection continues up to the point of latest redefinition of the object identified by 'qualifier', - * that is, up to the point of something like "foo.bar = ..." or "foo = ...". - * @param qualifier qualifying names, outermost first; must not be empty. - */ - public AssignmentCollectProcessor(@NotNull List qualifier) { - assert qualifier.size() > 0; - my_qualifier = qualifier; - my_result = new ArrayList(); - my_seen_names = new HashSet(); - } - - public boolean execute(final PsiElement element, final ResolveState state) { - if (element instanceof PyAssignmentStatement) { - final PyAssignmentStatement assignment = (PyAssignmentStatement)element; - for (PyExpression ex : assignment.getTargets()) { - if (ex instanceof PyTargetExpression) { - final PyTargetExpression target = (PyTargetExpression)ex; - List quals = unwindQualifiers(target); - if (quals != null) { - if (quals.size() == my_qualifier.size()+1 && pathsMatch(quals, my_qualifier)) { - // a new attribute follows last qualifier; collect it. - PyTargetExpression last_elt = quals.get(quals.size() - 1); // last item is the outermost, new, attribute. - String last_elt_name = last_elt.getName(); - if (!my_seen_names.contains(last_elt_name)) { // no dupes, only remember the latest - my_result.add(last_elt); - my_seen_names.add(last_elt_name); - } - } - else if (quals.size() < my_qualifier.size()+1 && pathsMatch(my_qualifier, quals)) { - // qualifier(s) get redefined; collect no more. - return false; - } - } - } - - } - } - return true; // nothing interesting found, continue - } - - /** - * @return a collection of exressions (parts of assignment expressions) where new attributes were defined. E.g. for "a.b.c = 1", - * the expression for 'c' is in the result. - */ - @NotNull - public Collection getResult() { - return my_result; - } - - public T getHint(final Class hintClass) { - return null; - } - - public void handleEvent(final Event event, final Object associated) { - // empty - } - } -} diff --git a/python/src/com/jetbrains/python/psi/PyUtil.java b/python/src/com/jetbrains/python/psi/PyUtil.java index ac4e6a9cae14..75e02d67e3b1 100644 --- a/python/src/com/jetbrains/python/psi/PyUtil.java +++ b/python/src/com/jetbrains/python/psi/PyUtil.java @@ -42,8 +42,8 @@ import java.util.Comparator; import java.util.List; public class PyUtil { - private PyUtil() { - } + private PyUtil() { + } public static void ensureWritable(PsiElement element) { PsiDocumentManager docmgr = PsiDocumentManager.getInstance( @@ -358,4 +358,31 @@ public class PyUtil { point, Balloon.Position.above ); } + + @NonNls + /** + * Returns a quoted string representation, or "null". + */ + public static String nvl(Object s) { + if (s != null) { + return "'" + s.toString() + "'"; + } + else { + return "null"; + } + } + + public static void addListNode(PsiElement target, PsiElement source, ASTNode beforeThis, boolean isFirst, boolean isLast) { + ensureWritable(target); + ASTNode node = target.getNode(); + assert node != null; + ASTNode itemNode = source.getNode(); + assert itemNode != null; + Project project = target.getProject(); + PyElementGenerator gen = PythonLanguage.getInstance().getElementGenerator(); + if (! isFirst) node.addChild(gen.createComma(project), beforeThis); + node.addChild(itemNode, beforeThis); + if (! isLast) node.addChild(gen.createComma(project), beforeThis); + } + } diff --git a/python/src/com/jetbrains/python/psi/impl/PyScopeProcessor.java b/python/src/com/jetbrains/python/psi/impl/PyAsScopeProcessor.java similarity index 76% rename from python/src/com/jetbrains/python/psi/impl/PyScopeProcessor.java rename to python/src/com/jetbrains/python/psi/impl/PyAsScopeProcessor.java index 7d641d83e5fd..755509284098 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyScopeProcessor.java +++ b/python/src/com/jetbrains/python/psi/impl/PyAsScopeProcessor.java @@ -6,6 +6,6 @@ import com.intellij.psi.scope.PsiScopeProcessor; /** * @author yole */ -public interface PyScopeProcessor extends PsiScopeProcessor { +public interface PyAsScopeProcessor extends PsiScopeProcessor { boolean execute(PsiElement element, String asName); } diff --git a/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java b/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java index a5af03865941..aecfa71c5397 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java @@ -27,6 +27,8 @@ import com.jetbrains.python.PyElementTypes; import com.jetbrains.python.PyNames; import com.jetbrains.python.PyTokenTypes; import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.resolve.PyResolveUtil; +import com.jetbrains.python.psi.resolve.VariantsProcessor; import com.jetbrains.python.psi.stubs.PyClassStub; import com.jetbrains.python.psi.stubs.PyFunctionStub; import com.jetbrains.python.validation.DocStringAnnotator; @@ -326,7 +328,7 @@ public class PyClassImpl extends PyPresentableElementImpl implement if (!processor.execute(expr, substitutor)) return false; } // - if (processor instanceof PyResolveUtil.VariantsProcessor) { + if (processor instanceof VariantsProcessor) { return true; } return processor.execute(this, substitutor); diff --git a/python/src/com/jetbrains/python/psi/impl/PyFileImpl.java b/python/src/com/jetbrains/python/psi/impl/PyFileImpl.java index 60a468bc1e03..989415d68f4f 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyFileImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyFileImpl.java @@ -18,6 +18,7 @@ package com.jetbrains.python.psi.impl; import com.intellij.extapi.psi.PsiFileBase; import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.FileViewProvider; import com.intellij.psi.PsiElement; @@ -31,6 +32,8 @@ import com.jetbrains.python.PyElementTypes; import com.jetbrains.python.PythonFileType; import com.jetbrains.python.PythonLanguage; import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.resolve.PyResolveUtil; +import com.jetbrains.python.psi.resolve.ResolveProcessor; import com.jetbrains.python.psi.types.PyClassType; import com.jetbrains.python.psi.types.PyType; import org.jetbrains.annotations.NotNull; @@ -151,6 +154,13 @@ public class PyFileImpl extends PsiFileBase implements PyFile, PyExpression { return getTopLevelItems(PyElementTypes.TARGET_EXPRESSION, PyTargetExpression.class); } + public PsiElement findExportedName(String name) { + // dull plain resolve, as fast as stub index or better + ResolveProcessor proc = new ResolveProcessor(name); + PyResolveUtil.treeCrawlUp(proc, true, getLastChild()); + return proc.getResult(); + } + public List getImportTargets() { List ret = new ArrayList(); List imports = getTopLevelItems(PyElementTypes.IMPORT_STATEMENT, PyImportStatement.class); diff --git a/python/src/com/jetbrains/python/psi/impl/PyImportElementImpl.java b/python/src/com/jetbrains/python/psi/impl/PyImportElementImpl.java index de21f0554f35..2fe86b8e0861 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyImportElementImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyImportElementImpl.java @@ -25,6 +25,8 @@ import com.intellij.psi.scope.PsiScopeProcessor; import com.intellij.psi.util.PsiTreeUtil; 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 org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -54,6 +56,16 @@ public class PyImportElementImpl extends PyElementImpl implements PyImportElemen return (PyTargetExpression)asNameNode.getPsi(); } + @Nullable + public String getVisibleName() { + PyTargetExpression asname = getAsName(); + if (asname != null) return asname.getName(); + for (PyElement name_elt : iterateNames()) { + return name_elt.getName(); // first to come must be right + } + return null; // we might have not found any names + } + @Override public boolean processDeclarations(@NotNull final PsiScopeProcessor processor, @NotNull final ResolveState state, final PsiElement lastParent, @NotNull final PsiElement place) { @@ -65,10 +77,10 @@ public class PyImportElementImpl extends PyElementImpl implements PyImportElemen if (importRef != null) { final PsiElement element = importRef.resolve(); if (element != null) { - if (processor instanceof PyScopeProcessor) { + if (processor instanceof PyAsScopeProcessor) { PyTargetExpression asName = getAsName(); if (asName != null) { - return ((PyScopeProcessor) processor).execute(element, asName.getText()); // might resolve to asName to show the source of name + return ((PyAsScopeProcessor) processor).execute(element, asName.getText()); // might resolve to asName to show the source of name } // maybe the incoming name is qualified PyReferenceExpression place_ref = PsiTreeUtil.getChildOfType(place, PyReferenceExpression.class); diff --git a/python/src/com/jetbrains/python/psi/impl/PyParameterImpl.java b/python/src/com/jetbrains/python/psi/impl/PyParameterImpl.java index 0f8afe7db83a..a77fd8a3ea95 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyParameterImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyParameterImpl.java @@ -102,6 +102,19 @@ public class PyParameterImpl extends PyPresentableElementImpl i return null; } + @NotNull + public String getRepr(boolean includeDefaultValue) { + StringBuffer sb = new StringBuffer(); + if (isPositionalContainer()) sb.append("*"); + else if (isKeywordContainer()) sb.append("**"); + sb.append(getName()); + if (includeDefaultValue) { + PyExpression default_v = getDefaultValue(); + if (default_v != null) sb.append("=").append(PyUtil.getReadableRepr(default_v, true)); + } + return sb.toString(); + } + public Icon getIcon(final int flags) { return Icons.PARAMETER_ICON; } diff --git a/python/src/com/jetbrains/python/psi/impl/PyParameterListImpl.java b/python/src/com/jetbrains/python/psi/impl/PyParameterListImpl.java index 8fd3e0f8d925..9298203d2d78 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyParameterListImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyParameterListImpl.java @@ -54,21 +54,7 @@ public class PyParameterListImpl extends PyBaseElementImpl PyUtil.ensureWritable(this); ASTNode beforeWhat = paren.getNode(); // the closing bracket will be this PyParameter[] params = getParameters(); - addItemNode(param, beforeWhat, true, params.length == 0); - } - } - - // TODO: open for general usage by all list-like structurtes - private void addItemNode(PyParameter item, ASTNode beforeThis, boolean isFirst, boolean isLast) { - PyUtil.ensureWritable(this); - ASTNode node = getNode(); - ASTNode itemNode = item.getNode(); - if (! isFirst) { - node.addChild(getLanguage().getElementGenerator().createComma(getProject()), beforeThis); - } - node.addChild(itemNode, beforeThis); - if (! isLast) { - node.addChild(getLanguage().getElementGenerator().createComma(getProject()), beforeThis); + PyUtil.addListNode(this, param, beforeWhat, true, params.length == 0); } } diff --git a/python/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java b/python/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java index c9b6b23450d6..1c1169e601a4 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyReferenceExpressionImpl.java @@ -36,6 +36,7 @@ import com.jetbrains.python.PyIcons; import com.jetbrains.python.PyNames; import com.jetbrains.python.PyTokenTypes; import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.resolve.*; import com.jetbrains.python.psi.types.PyClassType; import com.jetbrains.python.psi.types.PyModuleType; import com.jetbrains.python.psi.types.PyNoneType; @@ -204,7 +205,7 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere // here we have an unqualified expr. it may be defined: // ...in current file - PyResolveUtil.ResolveProcessor processor = new PyResolveUtil.ResolveProcessor(referencedName); + ResolveProcessor processor = new ResolveProcessor(referencedName); PsiElement uexpr = PyResolveUtil.treeCrawlUp(processor, this); if ((uexpr != null)) { if ((uexpr instanceof PyClass)) { @@ -230,7 +231,7 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere if (uexpr == null) { // ...as a builtin symbol PyFile bfile = PyBuiltinCache.getInstance(getProject()).getBuiltinsFile(); - uexpr = PyResolveUtil.treeCrawlUp(new PyResolveUtil.ResolveProcessor(referencedName), true, bfile); + uexpr = PyResolveUtil.treeCrawlUp(new ResolveProcessor(referencedName), true, bfile); } if (uexpr == null) { uexpr = PyResolveUtil.resolveOffContext(this); @@ -251,8 +252,7 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere private static Collection collectAssignedAttributes(PyQualifiedExpression qualifier) { List qualifier_path = PyResolveUtil.unwindQualifiers(qualifier); if (qualifier_path != null) { - PyResolveUtil.AssignmentCollectProcessor proc = - new PyResolveUtil.AssignmentCollectProcessor(qualifier_path) + AssignmentCollectProcessor proc = new AssignmentCollectProcessor(qualifier_path) ; PyResolveUtil.treeCrawlUp(proc, qualifier); return proc.getResult(); @@ -391,14 +391,14 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere } // include our own names - final PyResolveUtil.VariantsProcessor processor = new PyResolveUtil.VariantsProcessor(); + final VariantsProcessor processor = new VariantsProcessor(); PyResolveUtil.treeCrawlUp(processor, this); // names from here // scan all "import *" and include names provided by them - PyResolveUtil.CollectProcessor collect_proc; - collect_proc = new PyResolveUtil.CollectProcessor(PyStarImportElement.class); + CollectProcessor collect_proc; + collect_proc = new CollectProcessor(PyStarImportElement.class); PyResolveUtil.treeCrawlUp(collect_proc, this); - List stars = collect_proc.getResult(); - for (PyStarImportElement star_elt : stars) { + List stars = collect_proc.getResult(); + for (PsiElement star_elt : stars) { final PyFromImportStatement from_import_stmt = (PyFromImportStatement)star_elt.getParent(); if (from_import_stmt != null) { final PyReferenceExpression import_src = from_import_stmt.getImportSource(); diff --git a/python/src/com/jetbrains/python/psi/impl/PyStarImportElementImpl.java b/python/src/com/jetbrains/python/psi/impl/PyStarImportElementImpl.java index 738c00e583a3..a928e5ba5b6c 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyStarImportElementImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyStarImportElementImpl.java @@ -1,6 +1,7 @@ package com.jetbrains.python.psi.impl; import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.resolve.PyResolveUtil; 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/impl/ResolveImportUtil.java index 3337195fb207..5ffd27cd9d39 100644 --- a/python/src/com/jetbrains/python/psi/impl/ResolveImportUtil.java +++ b/python/src/com/jetbrains/python/psi/impl/ResolveImportUtil.java @@ -13,6 +13,9 @@ 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 org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.NotNull; @@ -370,13 +373,13 @@ public class ResolveImportUtil { public static PsiElement resolveChild(final PsiElement parent, final String referencedName, final PsiFile containingFile, boolean fileOnly) { PsiDirectory dir = null; PsiElement ret = null; - PyResolveUtil.ResolveProcessor processor = null; + ResolveProcessor processor = null; if (parent instanceof PyFile) { boolean is_dir = (parent.getCopyableUserData(PyFile.KEY_IS_DIRECTORY) == Boolean.TRUE); PyFile pfparent = (PyFile)parent; if (! is_dir) { // look for name in the file: - processor = new PyResolveUtil.ResolveProcessor(referencedName); + processor = new ResolveProcessor(referencedName); //ret = PyResolveUtil.treeWalkUp(processor, parent, null, importRef); ret = PyResolveUtil.treeCrawlUp(processor, true, parent); if (ret != null) return ret; @@ -407,8 +410,7 @@ public class ResolveImportUtil { @Nullable private static PsiElement resolveInDirectory(final String referencedName, final PsiFile containingFile, - final PsiDirectory dir, - PyResolveUtil.ResolveProcessor processor) { + final PsiDirectory dir, ResolveProcessor processor) { final PsiFile file = dir.findFile(referencedName + PY_SUFFIX); if (file != null) return file; final PsiDirectory subdir = dir.findSubdirectory(referencedName); @@ -417,7 +419,7 @@ public class ResolveImportUtil { final PsiFile initPy = dir.findFile(INIT_PY); if (initPy == containingFile) return null; // don't dive into the file we're in if (initPy != null) { - if (processor == null) processor = new PyResolveUtil.ResolveProcessor(referencedName); // should not normally happen + if (processor == null) processor = new ResolveProcessor(referencedName); // should not normally happen return PyResolveUtil.treeCrawlUp(processor, true, initPy);//PyResolveUtil.treeWalkUp(processor, initPy, null, importRef); } } @@ -440,7 +442,7 @@ public class ResolveImportUtil { if (src != null) { PsiElement mod = src.resolve(); if (mod != null) { - final PyResolveUtil.VariantsProcessor processor = new PyResolveUtil.VariantsProcessor(); + final VariantsProcessor processor = new VariantsProcessor(); PyResolveUtil.treeCrawlUp(processor, true, mod); /* for (LookupElement le : processor.getResult()) { @@ -492,7 +494,7 @@ public class ResolveImportUtil { variants.addAll(visitor.getResult()); } - return variants.toArray(new String[variants.size()]); + return variants.toArray(new Object[variants.size()]); } } diff --git a/python/src/com/jetbrains/python/psi/resolve/AssignmentCollectProcessor.java b/python/src/com/jetbrains/python/psi/resolve/AssignmentCollectProcessor.java new file mode 100644 index 000000000000..4899785950da --- /dev/null +++ b/python/src/com/jetbrains/python/psi/resolve/AssignmentCollectProcessor.java @@ -0,0 +1,83 @@ +package com.jetbrains.python.psi.resolve; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.ResolveState; +import com.intellij.psi.scope.PsiScopeProcessor; +import com.jetbrains.python.psi.PyAssignmentStatement; +import com.jetbrains.python.psi.PyExpression; +import com.jetbrains.python.psi.PyTargetExpression; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +public class AssignmentCollectProcessor implements PsiScopeProcessor { + /** + * Collects all assignments in context above given element, if they match given naming pattern. + * Used to track creation of attributes by assignment (e.g in constructor). + */ + List my_qualifier; + List my_result; + Set my_seen_names; + + /** + * Creates an instance to collect assignments of attributes to the object identified by 'qualifier'. + * E.g. if qualifier = {"foo", "bar"} then assignments like "foo.bar.baz = ..." will be considered. + * The collection continues up to the point of latest redefinition of the object identified by 'qualifier', + * that is, up to the point of something like "foo.bar = ..." or "foo = ...". + * + * @param qualifier qualifying names, outermost first; must not be empty. + */ + public AssignmentCollectProcessor(@NotNull List qualifier) { + assert qualifier.size() > 0; + my_qualifier = qualifier; + my_result = new ArrayList(); + my_seen_names = new HashSet(); + } + + public boolean execute(final PsiElement element, final ResolveState state) { + if (element instanceof PyAssignmentStatement) { + final PyAssignmentStatement assignment = (PyAssignmentStatement)element; + for (PyExpression ex : assignment.getTargets()) { + if (ex instanceof PyTargetExpression) { + final PyTargetExpression target = (PyTargetExpression)ex; + List quals = PyResolveUtil.unwindQualifiers(target); + if (quals != null) { + if (quals.size() == my_qualifier.size() + 1 && PyResolveUtil.pathsMatch(quals, my_qualifier)) { + // a new attribute follows last qualifier; collect it. + PyTargetExpression last_elt = quals.get(quals.size() - 1); // last item is the outermost, new, attribute. + String last_elt_name = last_elt.getName(); + if (!my_seen_names.contains(last_elt_name)) { // no dupes, only remember the latest + my_result.add(last_elt); + my_seen_names.add(last_elt_name); + } + } + else if (quals.size() < my_qualifier.size() + 1 && PyResolveUtil.pathsMatch(my_qualifier, quals)) { + // qualifier(s) get redefined; collect no more. + return false; + } + } + } + + } + } + return true; // nothing interesting found, continue + } + + /** + * @return a collection of exressions (parts of assignment expressions) where new attributes were defined. E.g. for "a.b.c = 1", + * the expression for 'c' is in the result. + */ + @NotNull + public Collection getResult() { + return my_result; + } + + public T getHint(final Class hintClass) { + return null; + } + + public void handleEvent(final Event event, final Object associated) { + // empty + } + +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/psi/resolve/CollectProcessor.java b/python/src/com/jetbrains/python/psi/resolve/CollectProcessor.java new file mode 100644 index 000000000000..b84d6b88c873 --- /dev/null +++ b/python/src/com/jetbrains/python/psi/resolve/CollectProcessor.java @@ -0,0 +1,44 @@ +package com.jetbrains.python.psi.resolve; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.ResolveState; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class CollectProcessor implements PyClassScopeProcessor { + + Class[] my_collectables; + List my_result; + + public CollectProcessor(Class... collectables) { + my_collectables = collectables; + my_result = new ArrayList(); + } + + public boolean execute(final PsiElement element, final ResolveState state) { + for (Class cls : my_collectables) { + if (cls.isInstance(element)) { + my_result.add(element); + } + } + return true; // collect till we drop + } + + public T getHint(final Class hintClass) { + return null; + } + + public void handleEvent(final Event event, final Object associated) { + } + + public List getResult() { + return my_result; + } + + @NotNull + public Class[] getPossibleTargets() { + return my_collectables; + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/psi/resolve/MultiResolveProcessor.java b/python/src/com/jetbrains/python/psi/resolve/MultiResolveProcessor.java new file mode 100644 index 000000000000..1213b5a19d7b --- /dev/null +++ b/python/src/com/jetbrains/python/psi/resolve/MultiResolveProcessor.java @@ -0,0 +1,45 @@ +package com.jetbrains.python.psi.resolve; + +import com.intellij.psi.scope.PsiScopeProcessor; +import com.intellij.psi.*; +import com.jetbrains.python.psi.PyReferenceExpression; + +import java.util.List; +import java.util.ArrayList; + +public class MultiResolveProcessor implements PsiScopeProcessor { + private final String _name; + private final List _results = new ArrayList(); + + public MultiResolveProcessor(String name) { + _name = name; + } + + public ResolveResult[] getResults() { + return _results.toArray(new ResolveResult[_results.size()]); + } + + public boolean execute(PsiElement element, ResolveState substitutor) { + if (element instanceof PsiNamedElement) { + if (_name.equals(((PsiNamedElement)element).getName())) { + _results.add(new PsiElementResolveResult(element)); + } + } + else if (element instanceof PyReferenceExpression) { + PyReferenceExpression expr = (PyReferenceExpression)element; + String referencedName = expr.getReferencedName(); + if (referencedName != null && referencedName.equals(_name)) { + _results.add(new PsiElementResolveResult(element)); + } + } + + return true; + } + + public T getHint(Class hintClass) { + return null; + } + + public void handleEvent(Event event, Object associated) { + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/psi/resolve/PyAsScopeProcessor.java b/python/src/com/jetbrains/python/psi/resolve/PyAsScopeProcessor.java new file mode 100644 index 000000000000..5536355a12b7 --- /dev/null +++ b/python/src/com/jetbrains/python/psi/resolve/PyAsScopeProcessor.java @@ -0,0 +1,11 @@ +package com.jetbrains.python.psi.resolve; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.scope.PsiScopeProcessor; + +/** + * @author yole + */ +public interface PyAsScopeProcessor extends PsiScopeProcessor { + boolean execute(PsiElement element, String asName); +} diff --git a/python/src/com/jetbrains/python/psi/resolve/PyClassScopeProcessor.java b/python/src/com/jetbrains/python/psi/resolve/PyClassScopeProcessor.java new file mode 100644 index 000000000000..57c4b8dabb28 --- /dev/null +++ b/python/src/com/jetbrains/python/psi/resolve/PyClassScopeProcessor.java @@ -0,0 +1,23 @@ +package com.jetbrains.python.psi.resolve; + +import com.intellij.psi.scope.PsiScopeProcessor; +import com.jetbrains.python.psi.NameDefiner; +import org.jetbrains.annotations.NotNull; + +/** + * Processor capable of giving multiple hints on what it's looking for. + * User: dcheryasov + * Date: Apr 19, 2009 + */ +public interface PyClassScopeProcessor extends PsiScopeProcessor { + /** + * @return classes of nodes that might be interesting for the processor. + * ??? Instances of NameDefiner are always considered interesting. + * ??? An empty list makes processor see only NameDefiners. + * @see com.jetbrains.python.psi.NameDefiner + */ + @NotNull + Class[] getPossibleTargets(); + + Class[] NAME_DEFINER_ONLY = {NameDefiner.class}; +} diff --git a/python/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java b/python/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java new file mode 100644 index 000000000000..d0dceae946d4 --- /dev/null +++ b/python/src/com/jetbrains/python/psi/resolve/PyResolveUtil.java @@ -0,0 +1,311 @@ +/* + * Copyright 2005 Pythonid Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS"; BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jetbrains.python.psi.resolve; + +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.ResolveState; +import com.intellij.psi.scope.PsiScopeProcessor; +import com.intellij.psi.util.PsiTreeUtil; +import com.jetbrains.python.psi.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * Ref resolution routines. + * User: yole + * Date: 14.06.2005 + */ +public class PyResolveUtil { + + private PyResolveUtil() { + } + + + /** + * Tries to find nearest parent that conceals nams definded inside it. Such elements are 'class' and 'def': + * anything defined within it does not seep to the namespace below them, but is concealed within. + * @param elt starting point of search. + * @return 'class' or 'def' element, or null if not found. + */ + @Nullable + public static PsiElement getConcealingParent(PsiElement elt) { + return PsiTreeUtil.getParentOfType(elt, PyClass.class, PyFunction.class); + } + + protected static PsiElement getInnermostChildOf(PsiElement elt) { + PsiElement feeler = elt; + PsiElement seeker; + seeker = feeler; + // find innermost last child of the subtree we're in + while (feeler != null) { + seeker = feeler; + feeler = feeler.getLastChild(); + } + return seeker; + } + + /** + * Returns closest previous node of given class, as input file would have it. + * @param elt node from which to look for a previous atatement. + * @param classes which class of the previous nodes to find. + * @return previous statement, or null. + */ + @Nullable + public static PsiElement getPrevNodeOf(PsiElement elt, Class... classes) { + PsiElement seeker = elt; + while (seeker != null) { + PsiElement feeler = seeker.getPrevSibling(); + if (feeler != null) { + seeker = getInnermostChildOf(feeler); + } + else { // we were the first subnode + // find something above the parent node we've not exhausted yet + seeker = seeker.getParent(); + if (seeker instanceof PyFile) return null; // all file nodes have been looked up, in vain + } + // ??? if (seeker instanceof NameDefiner) return seeker; + for (Class cls : classes) { + if (cls.isInstance(seeker)) return seeker; + } + } + // here elt is null or a PsiFile is not up in the parent chain. + return null; + } + + @Nullable + public static PsiElement getPrevNodeOf(PsiElement elt, PsiScopeProcessor proc) { + if (proc instanceof PyClassScopeProcessor) { + return getPrevNodeOf(elt, ((PyClassScopeProcessor)proc).getPossibleTargets()); + } + else return getPrevNodeOf(elt, PyClassScopeProcessor.NAME_DEFINER_ONLY); + } + + /** + * Crawls up the PSI tree, checking nodes as if crawling backwards through source lexemes. + * @param processor a visitor that says when the crawl is done and collects info. + * @param elt element from which we start (not checked by processor); if null, the search immediately returns null. + * @param roof if not null, search continues only below the roof and including it. + * @param fromunder if true, begin search not above elt, but from a [possibly imaginary] node right below elt; so elt gets analyzed, too. + * @return first element that the processor accepted. + */ + @Nullable + public static PsiElement treeCrawlUp(PsiScopeProcessor processor, boolean fromunder, PsiElement elt, PsiElement roof) { + if (elt == null) return null; // can't find anyway. + PsiElement seeker = elt; + PsiElement cap = getConcealingParent(elt); + do { + ProgressManager.getInstance().checkCanceled(); + if (fromunder) { + fromunder = false; // only honour fromunder once per call + seeker = getPrevNodeOf(getInnermostChildOf(seeker), processor); + } + else { // main case + seeker = getPrevNodeOf(seeker, processor); + } + // aren't we in the same defining assignment, global, etc? + if ((seeker instanceof NameDefiner) && ((NameDefiner)seeker).mustResolveOutside() && PsiTreeUtil.isAncestor(seeker, elt, true)) { + seeker = getPrevNodeOf(seeker, processor); + } + // maybe we're under a cap? + while (true) { + PsiElement local_cap = getConcealingParent(seeker); + if (local_cap == null) break; // seeker is in global context + if (local_cap == cap) break; // seeker is in the same context as elt + if ((cap != null) && PsiTreeUtil.isAncestor(local_cap, cap, true)) break; // seeker is in a context above elt's + if ( + (local_cap != elt) && // elt isn't the cap of seeker itself + ((cap == null) || !PsiTreeUtil.isAncestor(local_cap, cap, true)) // elt's cap is not under local cap + ) { // only look at local cap and above + if (local_cap instanceof NameDefiner) seeker = local_cap; + else seeker = getPrevNodeOf(local_cap, processor); + } + else break; // seeker is contextually under elt already + } + // are we still under the roof? + if ((roof != null) && (seeker != null) && ! PsiTreeUtil.isAncestor(roof, seeker, false)) return null; + // maybe we're capped by a class? + if (refersFromMethodToClass(cap, seeker)) continue; + // check what we got + if (seeker != null) { + if (!processor.execute(seeker, ResolveState.initial())) { + if (processor instanceof ResolveProcessor) { + return ((ResolveProcessor)processor).getResult(); + } + else return seeker; // can't point to exact element, but somewhere here + } + } + } while (seeker != null); + return null; + } + + @Nullable + public static PsiElement resolveOffContext(@NotNull PyReferenceExpression refex) { + // if we're under a cap, an external object that we want to use might be also defined below us. + // look through all contexts, closest first. + PsiElement ret = null; + PsiElement our_cap = getConcealingParent(refex); + ResolveProcessor proc = new ResolveProcessor(refex.getReferencedName()); // processor reusable till first hit + if (our_cap != null) { + PsiElement cap = our_cap; + while (true) { + cap = getConcealingParent(cap); + if (cap == null) cap = refex.getContainingFile(); + ret = treeCrawlUp(proc, true, cap); + if ((ret != null) && !PsiTreeUtil.isAncestor(our_cap, ret, true)) { // found something and it is below our cap + // maybe we're in a method, and what we found is in its class context? + if (! refersFromMethodToClass(our_cap, ret)) { + break; // not in method -> must be all right + } + } + if (cap instanceof PsiFile) break; // file level, can't try more + } + } + return ret; + } + + /** + * @param inner an element presumably inside a method within a class, or a method itself. + * @param outer an element presumably in the class context. + * @return true if an outer element is in a class context, while the inner is a method or function inside it. + * @see PyResolveUtil#getConcealingParent(com.intellij.psi.PsiElement) + */ + protected static boolean refersFromMethodToClass(final PsiElement inner, final PsiElement outer) { + return ( + (getConcealingParent(outer) instanceof PyClass) && // outer is in a class context + (PsiTreeUtil.getParentOfType(inner, PyFunction.class, false) != null) // inner is a function or method within the class + ); + } + + /** + * Crawls up the PSI tree, checking nodes as if crawling backwards through source lexemes. + * @param processor a visitor that says when the crawl is done and collects info. + * @param fromunder if true, search not above elt, but from a [possibly imaginary] node right below elt; so elt gets analyzed, too. + * @param elt element from which we start (not checked by processor); if null, the search immediately fails. + * @return first element that the processor accepted. + */ + @Nullable + public static PsiElement treeCrawlUp(PsiScopeProcessor processor, boolean fromunder, PsiElement elt) { + return treeCrawlUp(processor, fromunder, elt, null); + } + + /** + * Returns treeCrawlUp(processor, elt, false). A convenience method. + * @see PyResolveUtil#treeCrawlUp(com.intellij.psi.scope.PsiScopeProcessor,boolean,com.intellij.psi.PsiElement) + */ + @Nullable + public static PsiElement treeCrawlUp(PsiScopeProcessor processor, PsiElement elt) { + return treeCrawlUp(processor, false, elt); + } + + + /** + * Tries to match two [qualified] reference expression paths by names; target must be a 'sublist' of source to match. + * E.g., 'a.b.c.d' and 'a.b.c' would match, while 'a.b.c' and 'a.b.c.d' would not. Eqaully, 'a.b.c' and 'a.b.d' would not match. + * If either source or target is null, false is returned. + * @see #unwindQualifiers(PyQualifiedExpression) . + * @param source_path expression path to match (the longer list of qualifiers). + * @param target_path expression path to match against (hopeful sublist of qualifiers of source). + * @return true if source matches target. + */ + public static boolean pathsMatch(List source_path, List target_path) { + // turn qualifiers into lists + if ((source_path == null) || (target_path == null)) return false; + // compare until target is exhausted + Iterator source_iter = source_path.iterator(); + for (final T target_elt : target_path) { + if (source_iter.hasNext()) { + S source_elt = source_iter.next(); + if (!target_elt.getText().equals(source_elt.getText())) return false; + } + else return false; // source exhausted before target + } + return true; + } + + /** + * Unwinds a [multi-level] qualified expression into a path, as seen in source text, i.e. outermost qualifier first. + * If any qualifier happens to be not a PyQualifiedExpression, or expr is null, null is returned. + * @param expr an experssion to unwind. + * @return path as a list of ref expressions, or null. + */ + @Nullable + public static List unwindQualifiers(final T expr) { + final List path = new LinkedList(); + PyExpression maybe_step; + T step = expr; + try { + while (step != null) { + path.add(0, step); + maybe_step = step.getQualifier(); + step = (T)maybe_step; + } + } + catch (ClassCastException e) { + return null; + } + return path; + } + + public static String toPath(PyQualifiedExpression expr, String separator) { + if (expr == null) return ""; + List path = unwindQualifiers(expr); + if (path != null) { + StringBuilder buf = new StringBuilder(); + boolean is_not_first = false; + for (PyQualifiedExpression ex : path) { + if (is_not_first) buf.append(separator); + else is_not_first = true; + buf.append(ex.getName()); + } + return buf.toString(); + } + else { + String s = expr.getName(); + return s != null? s : ""; + } + } + + /** + * A simple interface allowing to filter processor results. + */ + public interface Filter { + /** + * @param target the object a processor is currently looking at. + * @return true if the object is acceptable as a processor result. + */ + boolean accept(Object target); + } + + public static class FilterNotInstance implements Filter { + Object instance; + + public FilterNotInstance(Object instance) { + this.instance = instance; + } + + public boolean accept(final Object target) { + return (instance != target); + } + } + +} diff --git a/python/src/com/jetbrains/python/psi/resolve/ResolveProcessor.java b/python/src/com/jetbrains/python/psi/resolve/ResolveProcessor.java new file mode 100644 index 000000000000..57cf90eb144d --- /dev/null +++ b/python/src/com/jetbrains/python/psi/resolve/ResolveProcessor.java @@ -0,0 +1,112 @@ +package com.jetbrains.python.psi.resolve; + +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiNamedElement; +import com.intellij.psi.ResolveState; +import com.intellij.psi.util.PsiTreeUtil; +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 org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class ResolveProcessor implements PyAsScopeProcessor { + private final String myName; + private PsiElement myResult = null; + private final List myDefiners; + + public ResolveProcessor(final String name) { + myName = name; + myDefiners = new ArrayList(2); // 1 is typical, 2 is sometimes, more is rare. + } + + public PsiElement getResult() { + return myResult; + } + + /** + * Adds a NameDefiner point which is a secondary resolution target. E.g. import statement for imported name. + * + * @param definer + */ + protected void addNameDefiner(NameDefiner definer) { + myDefiners.add(definer); + } + + public List getDefiners() { + return myDefiners; + } + + public String toString() { + return PyUtil.nvl(myName) + ", " + PyUtil.nvl(myResult); + } + + public boolean execute(PsiElement element, ResolveState substitutor) { + if (element instanceof PyFile) { + final VirtualFile file = ((PyFile)element).getVirtualFile(); + if (file != null) { + if (myName.equals(file.getNameWithoutExtension())) { + myResult = element; + return false; + } + else if (ResolveImportUtil.INIT_PY.equals(file.getName())) { + VirtualFile dir = file.getParent(); + if ((dir != null) && myName.equals(dir.getName())) { + myResult = element; + return false; + } + } + } + } + else if (element instanceof PsiNamedElement) { + if (myName.equals(((PsiNamedElement)element).getName())) { + myResult = element; + return false; + } + } + else if (element instanceof PyReferenceExpression) { + PyReferenceExpression expr = (PyReferenceExpression)element; + String referencedName = expr.getReferencedName(); + if (referencedName != null && referencedName.equals(myName)) { + myResult = element; + return false; + } + } + else if (element instanceof NameDefiner) { + final NameDefiner definer = (NameDefiner)element; + PsiElement by_name = definer.getElementNamed(myName); + if (by_name != null) { + myResult = by_name; + if (!PsiTreeUtil.isAncestor(element, by_name, true)) { // non-trivial definer + addNameDefiner(definer); + } + return false; + } + } + + return true; + } + + public boolean execute(final PsiElement element, final String asName) { + if (asName.equals(myName)) { + myResult = element; + return false; + } + return true; + } + + @Nullable + public T getHint(Class hintClass) { + return null; + } + + public void handleEvent(Event event, Object associated) { + } + + +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/psi/resolve/VariantsProcessor.java b/python/src/com/jetbrains/python/psi/resolve/VariantsProcessor.java new file mode 100644 index 000000000000..457846340121 --- /dev/null +++ b/python/src/com/jetbrains/python/psi/resolve/VariantsProcessor.java @@ -0,0 +1,110 @@ +package com.jetbrains.python.psi.resolve; + +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementFactory; +import com.intellij.codeInsight.lookup.LookupItem; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiNamedElement; +import com.intellij.psi.ResolveState; +import com.intellij.psi.scope.PsiScopeProcessor; +import com.jetbrains.python.psi.*; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class VariantsProcessor implements PsiScopeProcessor { + private final Map myVariants = new HashMap(); + + protected String my_notice; + + public VariantsProcessor() { + // empty + } + + public VariantsProcessor(final PyResolveUtil.Filter filter) { + my_filter = filter; + } + + protected PyResolveUtil.Filter my_filter; + + public void setNotice(@Nullable String notice) { + my_notice = notice; + } + + protected void setupItem(LookupItem item) { + if (my_notice != null) { + setItemNotice(item, my_notice); + } + } + + protected static void setItemNotice(final LookupItem item, String notice) { + item.setAttribute(item.TAIL_TEXT_ATTR, notice); + item.setAttribute(item.TAIL_TEXT_SMALL_ATTR, ""); + } + + public LookupElement[] getResult() { + final Collection variants = myVariants.values(); + return variants.toArray(new LookupElement[variants.size()]); + } + + public List getResultList() { + return new ArrayList(myVariants.values()); + } + + public boolean execute(PsiElement element, ResolveState substitutor) { + if (my_filter != null && !my_filter.accept(element)) return true; // skip whatever the filter rejects + // TODO: refactor to look saner; much code duplication + if (element instanceof PsiNamedElement) { + final PsiNamedElement psiNamedElement = (PsiNamedElement)element; + final String name = psiNamedElement.getName(); + if (!myVariants.containsKey(name)) { + final LookupItem lookup_item = (LookupItem)LookupElementFactory.getInstance().createLookupElement(psiNamedElement); + setupItem(lookup_item); + myVariants.put(name, lookup_item); + } + } + else if (element instanceof PyReferenceExpression) { + PyReferenceExpression expr = (PyReferenceExpression)element; + String referencedName = expr.getReferencedName(); + if (referencedName != null && !myVariants.containsKey(referencedName)) { + final LookupItem lookup_item = (LookupItem)LookupElementFactory.getInstance().createLookupElement(referencedName); + setupItem(lookup_item); + myVariants.put(referencedName, lookup_item); + } + } + else if (element instanceof NameDefiner) { + final NameDefiner definer = (NameDefiner)element; + for (PyElement expr : definer.iterateNames()) { + if (expr != null) { // NOTE: maybe rather have SingleIterables skip nulls outright? + String referencedName = expr.getName(); + if (referencedName != null && !myVariants.containsKey(referencedName)) { + final LookupItem lookup_item = (LookupItem)LookupElementFactory.getInstance().createLookupElement(referencedName); + setupItem(lookup_item); + if (definer instanceof PyImportElement) { // set notice to imported module name if needed + PsiElement maybe_from_import = definer.getParent(); + if (maybe_from_import instanceof PyFromImportStatement) { + final PyFromImportStatement from_import = (PyFromImportStatement)maybe_from_import; + PyReferenceExpression src = from_import.getImportSource(); + if (src != null) { + setItemNotice(lookup_item, " | " + src.getName()); + } + } + } + myVariants.put(referencedName, lookup_item); + } + } + } + } + + return true; + } + + @Nullable + public T getHint(Class hintClass) { + return null; + } + + public void handleEvent(Event event, Object associated) { + } + +} diff --git a/python/src/com/jetbrains/python/psi/types/PyClassType.java b/python/src/com/jetbrains/python/psi/types/PyClassType.java index 2a7167a634bc..d091a0b2fa3f 100644 --- a/python/src/com/jetbrains/python/psi/types/PyClassType.java +++ b/python/src/com/jetbrains/python/psi/types/PyClassType.java @@ -5,7 +5,9 @@ import com.intellij.psi.ResolveState; import com.jetbrains.python.psi.PyClass; import com.jetbrains.python.psi.PyExpression; import com.jetbrains.python.psi.PyReferenceExpression; -import com.jetbrains.python.psi.PyResolveUtil; +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.PyBuiltinCache; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -51,7 +53,7 @@ public class PyClassType implements PyType { @Nullable public PsiElement resolveMember(final String name) { if (myClass == null) return null; - PyResolveUtil.ResolveProcessor processor = new PyResolveUtil.ResolveProcessor(name); + ResolveProcessor processor = new ResolveProcessor(name); myClass.processDeclarations(processor, ResolveState.initial(), null, myClass); // our members are strictly within us. final PsiElement resolveResult = processor.getResult(); //final PsiElement resolveResult = PyResolveUtil.treeWalkUp(new PyResolveUtil.ResolveProcessor(name), myClass, null, null); @@ -94,7 +96,7 @@ public class PyClassType implements PyType { } public Object[] getCompletionVariants(final PyReferenceExpression referenceExpression) { - final PyResolveUtil.VariantsProcessor processor = new PyResolveUtil.VariantsProcessor(new PyResolveUtil.FilterNotInstance(myClass)); + final VariantsProcessor processor = new VariantsProcessor(new PyResolveUtil.FilterNotInstance(myClass)); myClass.processDeclarations(processor, ResolveState.initial(), null, referenceExpression); List ret = new ArrayList(); ret.addAll(processor.getResultList()); diff --git a/python/src/com/jetbrains/python/psi/types/PyModuleType.java b/python/src/com/jetbrains/python/psi/types/PyModuleType.java index 3a856adaeb39..d2f17f548101 100644 --- a/python/src/com/jetbrains/python/psi/types/PyModuleType.java +++ b/python/src/com/jetbrains/python/psi/types/PyModuleType.java @@ -4,7 +4,7 @@ import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.ResolveState; import com.jetbrains.python.psi.PyReferenceExpression; -import com.jetbrains.python.psi.PyResolveUtil; +import com.jetbrains.python.psi.resolve.VariantsProcessor; import com.jetbrains.python.psi.impl.ResolveImportUtil; import org.jetbrains.annotations.NotNull; @@ -43,7 +43,7 @@ public class PyModuleType implements PyType { // TODO: make it a PyClassType ref } public Object[] getCompletionVariants(final PyReferenceExpression referenceExpression) { - final PyResolveUtil.VariantsProcessor processor = new PyResolveUtil.VariantsProcessor(); + final VariantsProcessor processor = new VariantsProcessor(); myModule.processDeclarations(processor, ResolveState.initial(), null, referenceExpression); return processor.getResult(); } @@ -55,7 +55,7 @@ public class PyModuleType implements PyType { // TODO: make it a PyClassType ref } @NotNull - public Set getPossibleInstanceMembers() { + public static Set getPossibleInstanceMembers() { return ourPossibleFields; } diff --git a/python/src/com/jetbrains/python/validation/PyBuiltinAnnotator.java b/python/src/com/jetbrains/python/validation/PyBuiltinAnnotator.java index fa89b834180e..bfbbcc4d74dc 100644 --- a/python/src/com/jetbrains/python/validation/PyBuiltinAnnotator.java +++ b/python/src/com/jetbrains/python/validation/PyBuiltinAnnotator.java @@ -8,7 +8,7 @@ import com.jetbrains.python.PyNames; import com.jetbrains.python.PyTokenTypes; import com.jetbrains.python.psi.PyClass; import com.jetbrains.python.psi.PyReferenceExpression; -import com.jetbrains.python.psi.PyResolveUtil; +import com.jetbrains.python.psi.resolve.PyResolveUtil; import com.jetbrains.python.psi.impl.PyBuiltinCache; /** diff --git a/python/testData/inspections/AddToImportFromFoo.py b/python/testData/inspections/AddToImportFromFoo.py new file mode 100644 index 000000000000..cd946355f8e6 --- /dev/null +++ b/python/testData/inspections/AddToImportFromFoo.py @@ -0,0 +1,4 @@ +def foo(a): + pass + +bar = 2 diff --git a/python/testData/inspections/AddToImportFromList.py b/python/testData/inspections/AddToImportFromList.py new file mode 100644 index 000000000000..0e908235b701 --- /dev/null +++ b/python/testData/inspections/AddToImportFromList.py @@ -0,0 +1,3 @@ +from AddToImportFromFoo import bar + +foo # must get imported diff --git a/python/testData/inspections/AddToImportFromList_after.py b/python/testData/inspections/AddToImportFromList_after.py new file mode 100644 index 000000000000..ad6b4b707e87 --- /dev/null +++ b/python/testData/inspections/AddToImportFromList_after.py @@ -0,0 +1,3 @@ +from AddToImportFromFoo import bar, foo + +foo # must get imported diff --git a/python/testData/inspections/QualifyByImport.py b/python/testData/inspections/QualifyByImport.py new file mode 100644 index 000000000000..5b3905668ac5 --- /dev/null +++ b/python/testData/inspections/QualifyByImport.py @@ -0,0 +1,3 @@ +import QualifyByImportFoo + +foo # must be qualified diff --git a/python/testData/inspections/QualifyByImportFoo.py b/python/testData/inspections/QualifyByImportFoo.py new file mode 100644 index 000000000000..6b7a39b5f73c --- /dev/null +++ b/python/testData/inspections/QualifyByImportFoo.py @@ -0,0 +1 @@ +foo = "yes" diff --git a/python/testSrc/com/jetbrains/python/QuickFixTest.java b/python/testSrc/com/jetbrains/python/QuickFixTest.java index 53d4ace727b4..b6949259763d 100644 --- a/python/testSrc/com/jetbrains/python/QuickFixTest.java +++ b/python/testSrc/com/jetbrains/python/QuickFixTest.java @@ -19,7 +19,7 @@ import java.util.Collection; import java.util.List; /** - * Test action that various inspections add. + * Test actions that various inspections add. * User: dcheryasov * Date: Nov 29, 2008 12:47:08 AM */ @@ -29,6 +29,22 @@ public class QuickFixTest extends DaemonAnalyzerTestCase { doInspectionTest("AddImport.py", PyUnresolvedReferencesInspection.class, PyBundle.message("ACT.NAME.add.import"), true, true); } + public void testQualifyByImport() throws Exception { + doInspectionTest( + new String[]{"QualifyByImport.py", "QualifyByImportFoo.py"}, + PyUnresolvedReferencesInspection.class, "QualifyByImportFoo.foo?", true, false + ); + } + + public void testAddToImportFromList() throws Exception { + doInspectionTest( + new String[]{"AddToImportFromList.py", "AddToImportFromFoo.py"}, + PyUnresolvedReferencesInspection.class, "foo(a) from AddToImportFromFoo?", true, false + ); + } + + // TODO: add a test for multiple variants of above + public void testAddSelf() throws Exception { doInspectionTest("AddSelf.py", PyMethodParametersInspection.class, PyBundle.message("QFIX.add.parameter.self"), true, true); } @@ -92,7 +108,7 @@ public class QuickFixTest extends DaemonAnalyzerTestCase { } protected String getTestDataPath() { - return PathManager.getHomePath() + "/plugins/python/testData/"; + return PathManager.getHomePath() + "/plugins/python/testData/inspections/"; } protected void doInspectionTest(String testFileName, @@ -100,14 +116,31 @@ public class QuickFixTest extends DaemonAnalyzerTestCase { String quickFixName, boolean applyFix, boolean available + ) throws Exception { + doInspectionTest(new String[]{testFileName}, inspectionClass, quickFixName, applyFix, available); + } + + /** + * Runs daemon passes and looks for given fix within infos. + * @param testFiles names of files to participate; first is used for inspection and then for check by "_after". + * @param inspectionClass what inspection to run + * @param quickFixName how the resulting fix should be named (the human-readable name users see) + * @param applyFix true if the fix needs to be applied + * @param available true if the fix should be available, false if it should be explicitly not available. + * @throws Exception + */ + protected void doInspectionTest(String[] testFiles, + final Class inspectionClass, + String quickFixName, + boolean applyFix, + boolean available ) throws Exception { final LocalInspectionTool tool = (LocalInspectionTool)inspectionClass.newInstance(); enableInspectionTool(tool); - final String s = "inspections/" + testFileName; - configureByFile(s); + configureByFiles(null, testFiles); Collection infos = doDoTest(true, false); - doQuickFixTest(infos, quickFixName, applyFix, available, s); + doQuickFixTest(infos, quickFixName, applyFix, available, testFiles[0]); disableInspectionTool(tool.getShortName()); }