diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties index 4259ea2165b9..8289d9646d05 100644 --- a/python/src/com/jetbrains/python/PyBundle.properties +++ b/python/src/com/jetbrains/python/PyBundle.properties @@ -99,6 +99,9 @@ QFIX.docstring.insert.stub=Insert docstring #RemoveDecoratorQuickFix QFIX.remove.decorator=Remove decorator +#PyRenameUnresolvedRefQuickFix +QFIX.rename.unresolved.reference=Rename reference + # Intentions: INTN INTN.Family.convert.import.unqualify=Convert 'import module' to 'from module import' INTN.Family.convert.import.qualify=Convert 'from module import' to 'import module' diff --git a/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java b/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java index 58b0d2e8e1a0..f2959a613ab1 100644 --- a/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyUnresolvedReferencesInspection.java @@ -456,6 +456,7 @@ public class PyUnresolvedReferencesInspection extends PyInspection { if (parentFunction != null && decorator == null) { actions.add(new UnresolvedReferenceAddParameterQuickFix(refname)); } + actions.add(new PyRenameUnresolvedRefQuickFix()); } // unqualified: // may be module's diff --git a/python/src/com/jetbrains/python/inspections/quickfix/PyRenameUnresolvedRefQuickFix.java b/python/src/com/jetbrains/python/inspections/quickfix/PyRenameUnresolvedRefQuickFix.java new file mode 100644 index 000000000000..3790f0ab2e55 --- /dev/null +++ b/python/src/com/jetbrains/python/inspections/quickfix/PyRenameUnresolvedRefQuickFix.java @@ -0,0 +1,180 @@ + +package com.jetbrains.python.inspections.quickfix; + +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.codeInsight.template.*; +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorFactory; +import com.intellij.openapi.project.Project; +import com.intellij.psi.*; +import com.intellij.psi.util.PsiTreeUtil; +import com.jetbrains.python.PyBundle; +import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; +import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil; +import com.jetbrains.python.psi.PyRecursiveElementVisitor; +import com.jetbrains.python.psi.PyReferenceExpression; +import com.jetbrains.python.psi.impl.references.PyReferenceImpl; +import com.jetbrains.python.refactoring.PyRefactoringUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +/** + * User : ktisha + * + * Quick fix to rename unresolved references + */ +public class PyRenameUnresolvedRefQuickFix implements LocalQuickFix { + + @NotNull + @Override + public String getName() { + return PyBundle.message("QFIX.rename.unresolved.reference"); + } + + @Override + @NotNull + public String getFamilyName() { + return PyBundle.message("QFIX.rename.unresolved.reference"); + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + final PsiElement element = descriptor.getPsiElement(); + final PyReferenceExpression referenceExpression = PsiTreeUtil.getParentOfType(element, PyReferenceExpression.class); + if (referenceExpression == null) return; + + ScopeOwner parentScope = ScopeUtil.getScopeOwner(referenceExpression); + if (parentScope == null) return; + + List refs = collectExpressionsToRename(referenceExpression, parentScope); + + LookupElement[] items = collectLookupItems(referenceExpression, parentScope); + final String name = referenceExpression.getReferencedName(); + + ReferenceNameExpression refExpr = new ReferenceNameExpression(items, name); + TemplateBuilderImpl builder = (TemplateBuilderImpl)TemplateBuilderFactory.getInstance(). + createTemplateBuilder(parentScope); + for (PyReferenceExpression expr : refs) { + if (!expr.equals(referenceExpression)) { + builder.replaceElement(expr, name, name, false); + } + else { + builder.replaceElement(expr, name, refExpr, true); + } + } + + Editor editor = getEditor(project, element.getContainingFile()); + if (editor != null) { + editor.getCaretModel().moveToOffset(parentScope.getTextRange().getStartOffset()); + Template template = builder.buildInlineTemplate(); + TemplateManager.getInstance(project).startTemplate(editor, template); + } + } + + public static boolean isValidReference(final PsiReference reference) { + if (!(reference instanceof PyReferenceImpl)) return false; + ResolveResult[] results = ((PyReferenceImpl)reference).multiResolve(true); + if(results.length == 0) return false; + for (ResolveResult result : results) { + if (!result.isValidResult()) return false; + } + return true; + } + + + private static List collectExpressionsToRename(@NotNull final PyReferenceExpression expression, + @NotNull final ScopeOwner parentScope) { + + final List result = new ArrayList(); + PyRecursiveElementVisitor visitor = new PyRecursiveElementVisitor() { + @Override + public void visitPyReferenceExpression(PyReferenceExpression node) { + if (node.textMatches(expression) && !isValidReference(node.getReference())) { + result.add(node); + } + super.visitPyReferenceExpression(node); + } + }; + + parentScope.accept(visitor); + return result; + } + + @Nullable + private static Editor getEditor(@NotNull final Project project, @NotNull final PsiFile file) { + Document document = PsiDocumentManager.getInstance(project).getDocument(file); + if (document != null) { + final EditorFactory instance = EditorFactory.getInstance(); + if (instance == null) return null; + Editor[] editors = instance.getEditors(document); + if (editors.length > 0) + return editors[0]; + } + return null; + } + + private static LookupElement[] collectLookupItems(@NotNull final PyReferenceExpression referenceExpression, + @NotNull final ScopeOwner parentScope) { + Set items = new LinkedHashSet(); + + final Collection usedNames = PyRefactoringUtil.collectUsedNames(parentScope); + for (String name : usedNames) { + items.add(LookupElementBuilder.create(name)); + } + + return items.toArray(new LookupElement[items.size()]); + } + + private class ReferenceNameExpression extends Expression { + class HammingComparator implements Comparator { + @Override + public int compare(LookupElement lookupItem1, LookupElement lookupItem2) { + String s1 = lookupItem1.getLookupString(); + String s2 = lookupItem2.getLookupString(); + int diff1 = 0; + for (int i = 0; i < Math.min(s1.length(), myOldReferenceName.length()); i++) { + if (s1.charAt(i) != myOldReferenceName.charAt(i)) diff1++; + } + int diff2 = 0; + for (int i = 0; i < Math.min(s2.length(), myOldReferenceName.length()); i++) { + if (s2.charAt(i) != myOldReferenceName.charAt(i)) diff2++; + } + return diff1 - diff2; + } + } + + ReferenceNameExpression(LookupElement[] items, String oldReferenceName) { + myItems = items; + myOldReferenceName = oldReferenceName; + Arrays.sort(myItems, new HammingComparator()); + } + + LookupElement[] myItems; + private final String myOldReferenceName; + + @Override + public Result calculateResult(ExpressionContext context) { + if (myItems == null || myItems.length == 0) { + return new TextResult(myOldReferenceName); + } + return new TextResult(myItems[0].getLookupString()); + } + + @Override + public Result calculateQuickResult(ExpressionContext context) { + return null; + } + + @Override + public LookupElement[] calculateLookupItems(ExpressionContext context) { + if (myItems == null || myItems.length == 1) return null; + return myItems; + } + } +} diff --git a/python/testData/inspections/RenameUnresolvedReference.py b/python/testData/inspections/RenameUnresolvedReference.py new file mode 100644 index 000000000000..7981fe8271b3 --- /dev/null +++ b/python/testData/inspections/RenameUnresolvedReference.py @@ -0,0 +1,5 @@ + +def foo(y1): + y + 1 + print y + diff --git a/python/testData/inspections/RenameUnresolvedReference_after.py b/python/testData/inspections/RenameUnresolvedReference_after.py new file mode 100644 index 000000000000..34070f369ed8 --- /dev/null +++ b/python/testData/inspections/RenameUnresolvedReference_after.py @@ -0,0 +1,5 @@ + +def foo(y1): + y1 + 1 + print y1 + diff --git a/python/testSrc/com/jetbrains/python/PyQuickFixTest.java b/python/testSrc/com/jetbrains/python/PyQuickFixTest.java index a5244a2d97b4..e0ee6d07c252 100644 --- a/python/testSrc/com/jetbrains/python/PyQuickFixTest.java +++ b/python/testSrc/com/jetbrains/python/PyQuickFixTest.java @@ -347,6 +347,11 @@ public class PyQuickFixTest extends PyTestCase { PyBundle.message("QFIX.statement.effect.move.docstring"), true, true); } + public void testRenameUnresolvedReference() { //PY-6595 + doInspectionTest("RenameUnresolvedReference.py", PyUnresolvedReferencesInspection.class, + PyBundle.message("QFIX.rename.unresolved.reference"), true, true); + } + public void testSetFunctionToLiteral() { //PY-3120 setLanguageLevel(LanguageLevel.PYTHON27); doInspectionTest("SetFunctionToLiteral.py", PySetFunctionToLiteralInspection.class,