From 3856b5fecee198ed4d7050c44cea2b3400ebba59 Mon Sep 17 00:00:00 2001 From: Andrey Vlasovskikh Date: Mon, 28 Nov 2011 12:21:40 +0400 Subject: [PATCH] Update references when moving Python modules (PY-4379) --- python/src/META-INF/python-plugin-common.xml | 1 + .../classes/PyClassRefactoringUtil.java | 61 +++++++--- .../refactoring/move/PyMoveFileHandler.java | 104 ++++++++++++++++++ .../refactoring/move/module/after/src/a.py | 7 ++ .../move/module/after/src/p1/__init__.py | 0 .../move/module/after/src/p1/m1.py | 2 + .../move/module/after/src/p1/p2/__init__.py | 0 .../refactoring/move/module/before/src/a.py | 9 ++ .../move/module/before/src/p1/__init__.py | 0 .../move/module/before/src/p1/p2/__init__.py | 0 .../move/module/before/src/p1/p2/m1.py | 2 + .../python/refactoring/PyMoveTest.java | 44 +++++++- 12 files changed, 209 insertions(+), 21 deletions(-) create mode 100644 python/src/com/jetbrains/python/refactoring/move/PyMoveFileHandler.java create mode 100644 python/testData/refactoring/move/module/after/src/a.py create mode 100644 python/testData/refactoring/move/module/after/src/p1/__init__.py create mode 100644 python/testData/refactoring/move/module/after/src/p1/m1.py create mode 100644 python/testData/refactoring/move/module/after/src/p1/p2/__init__.py create mode 100644 python/testData/refactoring/move/module/before/src/a.py create mode 100644 python/testData/refactoring/move/module/before/src/p1/__init__.py create mode 100644 python/testData/refactoring/move/module/before/src/p1/p2/__init__.py create mode 100644 python/testData/refactoring/move/module/before/src/p1/p2/m1.py diff --git a/python/src/META-INF/python-plugin-common.xml b/python/src/META-INF/python-plugin-common.xml index 08ad91ac7121..e8f72f7bc332 100644 --- a/python/src/META-INF/python-plugin-common.xml +++ b/python/src/META-INF/python-plugin-common.xml @@ -334,6 +334,7 @@ + diff --git a/python/src/com/jetbrains/python/refactoring/classes/PyClassRefactoringUtil.java b/python/src/com/jetbrains/python/refactoring/classes/PyClassRefactoringUtil.java index 7f630a3ef93e..683642f623a8 100644 --- a/python/src/com/jetbrains/python/refactoring/classes/PyClassRefactoringUtil.java +++ b/python/src/com/jetbrains/python/refactoring/classes/PyClassRefactoringUtil.java @@ -194,7 +194,12 @@ public class PyClassRefactoringUtil { if (target == null) return; if (PyBuiltinCache.getInstance(target).hasInBuiltins(target)) return; if (PsiTreeUtil.isAncestor(node.getContainingFile(), target, false)) return; - insertImport(node, target, asName); + if (target instanceof PyFile) { + insertImport(node, target, asName, false); + } + else { + insertImport(node, target, asName); + } node.putCopyableUserData(ENCODED_IMPORT, null); node.putCopyableUserData(ENCODED_IMPORT_AS, null); } @@ -232,23 +237,28 @@ public class PyClassRefactoringUtil { public static void insertImport(PsiElement anchor, PsiNamedElement element, @Nullable String asName, boolean preferFromImport) { if (PyBuiltinCache.getInstance(element).hasInBuiltins(element)) return; final PsiFile newFile = element.getContainingFile(); - final VirtualFile vFile = newFile.getVirtualFile(); - assert vFile != null; final PsiFile file = anchor.getContainingFile(); if (newFile == file) return; - final PyQualifiedName qName = ResolveImportUtil.findCanonicalImportPath(element, anchor); - assert isValidQualifiedName(qName); - final String importableName = (qName != null) ? qName.toString() : null; + final PyQualifiedName qname = ResolveImportUtil.findCanonicalImportPath(element, anchor); + if (qname == null || !isValidQualifiedName(qname)) { + return; + } + final PyQualifiedName containingQName; + final String importedName; + if (element instanceof PyFile) { + containingQName = qname.removeLastComponent(); + importedName = qname.getLastComponent(); + } + else { + containingQName = qname; + importedName = getOriginalName(element); + } final AddImportHelper.ImportPriority priority = AddImportHelper.getImportPriority(anchor, newFile); - if (!preferFromImport || element instanceof PyFile) { - if (element instanceof PyFile) { - AddImportHelper.addImportStatement(file, importableName, asName, priority); - } else { - final String name = element.getName(); - AddImportHelper.addImportStatement(file, importableName + "." + name, asName, priority); - } - } else { - AddImportHelper.addImportFrom(file, importableName, element.getName(), asName, priority); + if (preferFromImport && !containingQName.getComponents().isEmpty()) { + AddImportHelper.addImportFrom(file, containingQName.toString(), importedName, asName, priority); + } + else { + AddImportHelper.addImportStatement(file, containingQName.append(importedName).toString(), asName, priority); } } @@ -306,7 +316,7 @@ public class PyClassRefactoringUtil { } public static void updateImportOfElement(PyImportStatementBase importStatement, PsiNamedElement element) { - final String name = element.getName(); + final String name = getOriginalName(element); if (name != null) { PyImportElement importElement = null; for (PyImportElement e: importStatement.getImportElements()) { @@ -327,8 +337,23 @@ public class PyClassRefactoringUtil { } @Nullable - private static String getOriginalName(PyImportElement e) { - final PyQualifiedName qname = e.getImportedQName(); + public static String getOriginalName(PsiNamedElement element) { + if (element instanceof PyFile) { + final PsiElement e = PyUtil.turnInitIntoDir(element); + if (e instanceof PsiFileSystemItem) { + final VirtualFile virtualFile = ((PsiFileSystemItem)e).getVirtualFile(); + if (virtualFile != null) { + return virtualFile.getNameWithoutExtension(); + } + } + return null; + } + return element.getName(); + } + + @Nullable + private static String getOriginalName(PyImportElement element) { + final PyQualifiedName qname = element.getImportedQName(); if (qname != null && qname.getComponentCount() > 0) { return qname.getComponents().get(0); } diff --git a/python/src/com/jetbrains/python/refactoring/move/PyMoveFileHandler.java b/python/src/com/jetbrains/python/refactoring/move/PyMoveFileHandler.java new file mode 100644 index 000000000000..3bce02aef7a3 --- /dev/null +++ b/python/src/com/jetbrains/python/refactoring/move/PyMoveFileHandler.java @@ -0,0 +1,104 @@ +package com.jetbrains.python.refactoring.move; + +import com.intellij.openapi.util.Key; +import com.intellij.psi.*; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFileHandler; +import com.intellij.usageView.UsageInfo; +import com.intellij.util.IncorrectOperationException; +import com.jetbrains.python.codeInsight.imports.PyImportOptimizer; +import com.jetbrains.python.psi.*; +import com.jetbrains.python.psi.impl.PyQualifiedName; +import com.jetbrains.python.psi.resolve.ResolveImportUtil; +import com.jetbrains.python.refactoring.PyRefactoringUtil; +import com.jetbrains.python.refactoring.classes.PyClassRefactoringUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author vlan + */ +public class PyMoveFileHandler extends MoveFileHandler { + private static final Key REFERENCED_ELEMENT = Key.create("PY_REFERENCED_ELEMENT"); + + @Override + public boolean canProcessElement(PsiFile element) { + return element instanceof PyFile; + } + + @Override + public void prepareMovedFile(PsiFile file, PsiDirectory moveDestination, Map oldToNewMap) { + // TODO: Update relative imports + } + + @Override + public List findUsages(PsiFile file, PsiDirectory newParent, boolean searchInComments, boolean searchInNonJavaFiles) { + if (file != null) { + final List usages = PyRefactoringUtil.findUsages(file); + for (UsageInfo usage : usages) { + final PsiElement element = usage.getElement(); + if (element != null) { + element.putCopyableUserData(REFERENCED_ELEMENT, file); + } + } + return usages; + } + return null; + } + + @Override + public void retargetUsages(List usages, Map oldToNewMap) { + final Set updatedFiles = new HashSet(); + for (UsageInfo usage : usages) { + final PsiElement element = usage.getElement(); + if (element != null) { + // TODO: Check if the element still exists/valid + final PsiNamedElement newElement = element.getCopyableUserData(REFERENCED_ELEMENT); + element.putCopyableUserData(REFERENCED_ELEMENT, null); + if (newElement != null) { + final PsiFile file = element.getContainingFile(); + final PyImportStatementBase importStmt = PsiTreeUtil.getParentOfType(usage.getElement(), PyImportStatementBase.class); + // TODO: Retarget qualified expressions in docstrings + if (importStmt != null) { + PyClassRefactoringUtil.updateImportOfElement(importStmt, newElement); + final PyQualifiedName newElementName = ResolveImportUtil.findCanonicalImportPath(newElement, element); + replaceWithQualifiedExpression(element, newElementName); + updatedFiles.add(file); + } + else if (element instanceof PyReferenceExpression) { + final PyQualifiedName newElementName = PyQualifiedName.fromComponents(PyClassRefactoringUtil.getOriginalName(newElement)); + replaceWithQualifiedExpression(element, newElementName); + PyClassRefactoringUtil.insertImport(element, newElement, null); + updatedFiles.add(file); + } + } + } + } + if (!updatedFiles.isEmpty()) { + final PyImportOptimizer optimizer = new PyImportOptimizer(); + for (PsiFile file : updatedFiles) { + optimizer.processFile(file).run(); + } + } + } + + private static void replaceWithQualifiedExpression(@NotNull PsiElement oldElement, + @Nullable PyQualifiedName newElementName) { + if (newElementName != null && PyClassRefactoringUtil.isValidQualifiedName(newElementName)) { + final PyElementGenerator generator = PyElementGenerator.getInstance(oldElement.getProject()); + final PsiElement newElement = generator.createExpressionFromText(LanguageLevel.forElement(oldElement), newElementName.toString()); + if (newElement != null) { + oldElement.replace(newElement); + } + } + } + + @Override + public void updateMovedFile(PsiFile file) throws IncorrectOperationException { + } +} diff --git a/python/testData/refactoring/move/module/after/src/a.py b/python/testData/refactoring/move/module/after/src/a.py new file mode 100644 index 000000000000..821e48bdf42b --- /dev/null +++ b/python/testData/refactoring/move/module/after/src/a.py @@ -0,0 +1,7 @@ +from p1 import m1 +from p1.m1 import foo + +m1.foo +m1.bar +m1 +foo \ No newline at end of file diff --git a/python/testData/refactoring/move/module/after/src/p1/__init__.py b/python/testData/refactoring/move/module/after/src/p1/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/refactoring/move/module/after/src/p1/m1.py b/python/testData/refactoring/move/module/after/src/p1/m1.py new file mode 100644 index 000000000000..cb19528f954f --- /dev/null +++ b/python/testData/refactoring/move/module/after/src/p1/m1.py @@ -0,0 +1,2 @@ +foo = 42 +bar = -1 \ No newline at end of file diff --git a/python/testData/refactoring/move/module/after/src/p1/p2/__init__.py b/python/testData/refactoring/move/module/after/src/p1/p2/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/refactoring/move/module/before/src/a.py b/python/testData/refactoring/move/module/before/src/a.py new file mode 100644 index 000000000000..89193abbbec7 --- /dev/null +++ b/python/testData/refactoring/move/module/before/src/a.py @@ -0,0 +1,9 @@ +from p1.p2 import m1 +from p1.p2.m1 import foo +import p1.p2.m1 +import p1 + +p1.p2.m1.foo +m1.bar +m1 +foo \ No newline at end of file diff --git a/python/testData/refactoring/move/module/before/src/p1/__init__.py b/python/testData/refactoring/move/module/before/src/p1/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/refactoring/move/module/before/src/p1/p2/__init__.py b/python/testData/refactoring/move/module/before/src/p1/p2/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/testData/refactoring/move/module/before/src/p1/p2/m1.py b/python/testData/refactoring/move/module/before/src/p1/p2/m1.py new file mode 100644 index 000000000000..cb19528f954f --- /dev/null +++ b/python/testData/refactoring/move/module/before/src/p1/p2/m1.py @@ -0,0 +1,2 @@ +foo = 42 +bar = -1 \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/refactoring/PyMoveTest.java b/python/testSrc/com/jetbrains/python/refactoring/PyMoveTest.java index 6f2cad79bdce..8451e7f720a3 100644 --- a/python/testSrc/com/jetbrains/python/refactoring/PyMoveTest.java +++ b/python/testSrc/com/jetbrains/python/refactoring/PyMoveTest.java @@ -1,9 +1,10 @@ package com.jetbrains.python.refactoring; +import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.PsiDocumentManager; -import com.intellij.psi.PsiNamedElement; +import com.intellij.psi.*; +import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFilesOrDirectoriesProcessor; import com.intellij.testFramework.PlatformTestUtil; import com.intellij.util.SystemProperties; import com.jetbrains.python.PythonTestUtil; @@ -69,7 +70,44 @@ public class PyMoveTest extends PyTestCase { doMoveSymbolTest("B", "b.py"); } - private void doMoveSymbolTest(final String symbolName, final String toFileName) { + // PY-4379 + public void testModule() { + doMoveFileTest("p1/p2/m1.py", "p1"); + } + + private void doMoveFileTest(String fileName, String toDirName) { + Project project = myFixture.getProject(); + PsiManager manager = PsiManager.getInstance(project); + + String root = "/refactoring/move/" + getTestName(true); + String rootBefore = root + "/before/src"; + String rootAfter = root + "/after/src"; + + VirtualFile dir1 = myFixture.copyDirectoryToProject(rootBefore, ""); + PsiDocumentManager.getInstance(project).commitAllDocuments(); + + VirtualFile virtualFile = dir1.findFileByRelativePath(fileName); + assertNotNull(virtualFile); + PsiElement file = manager.findFile(virtualFile); + if (file == null) { + file = manager.findDirectory(virtualFile); + } + assertNotNull(file); + VirtualFile toVirtualDir = dir1.findFileByRelativePath(toDirName); + assertNotNull(toVirtualDir); + PsiDirectory toDir = manager.findDirectory(toVirtualDir); + new MoveFilesOrDirectoriesProcessor(project, new PsiElement[] {file}, toDir, false, false, null, null).run(); + + VirtualFile dir2 = getVirtualFileByName(PythonTestUtil.getTestDataPath() + rootAfter); + try { + PlatformTestUtil.assertDirectoriesEqual(dir2, dir1, null); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void doMoveSymbolTest(String symbolName, String toFileName) { String root = "/refactoring/move/" + getTestName(true); String rootBefore = root + "/before/src"; String rootAfter = root + "/after/src";