package com.python.pydev.refactoring.wizards.rename; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.TreeSet; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jface.text.IDocument; import org.eclipse.ltk.core.refactoring.RefactoringStatus; import org.eclipse.text.edits.ReplaceEdit; import org.eclipse.text.edits.TextEdit; import org.python.pydev.core.FullRepIterable; import org.python.pydev.core.ICompletionState; import org.python.pydev.core.IDefinition; import org.python.pydev.core.IPythonNature; import org.python.pydev.core.docutils.PySelection; import org.python.pydev.core.log.Log; import org.python.pydev.core.structure.CompletionRecursionException; import org.python.pydev.editor.autoedit.DefaultIndentPrefs; import org.python.pydev.editor.codecompletion.revisited.CompletionCache; import org.python.pydev.editor.codecompletion.revisited.CompletionStateFactory; import org.python.pydev.editor.codecompletion.revisited.modules.SourceModule; import org.python.pydev.editor.refactoring.PyRefactoringFindDefinition; import org.python.pydev.parser.jython.SimpleNode; import org.python.pydev.parser.jython.ast.Attribute; import org.python.pydev.parser.jython.ast.ClassDef; import org.python.pydev.parser.jython.ast.FunctionDef; import org.python.pydev.parser.jython.ast.Import; import org.python.pydev.parser.jython.ast.ImportFrom; import org.python.pydev.parser.jython.ast.Module; import org.python.pydev.parser.jython.ast.NameTok; import org.python.pydev.parser.jython.ast.NameTokType; import org.python.pydev.parser.jython.ast.VisitorBase; import org.python.pydev.parser.jython.ast.aliasType; import org.python.pydev.parser.jython.ast.stmtType; import org.python.pydev.parser.prettyprinterv2.MakeAstValidForPrettyPrintingVisitor; import org.python.pydev.parser.prettyprinterv2.PrettyPrinterPrefsV2; import org.python.pydev.parser.prettyprinterv2.PrettyPrinterV2; import org.python.pydev.parser.visitors.NodeUtils; import org.python.pydev.parser.visitors.scope.ASTEntry; import org.python.pydev.shared_core.string.StringUtils; import org.python.pydev.shared_core.structure.FastStack; import org.python.pydev.shared_core.structure.Tuple; import org.python.pydev.shared_core.utils.ArrayUtils; import com.python.pydev.analysis.scopeanalysis.ScopeAnalysis; public class MatchImportsVisitor extends VisitorBase { private static final class ImportFromModPartRenameAstEntry extends ImportRenameAstEntry { /** * I.e.: the name which was matched (it may be different from the import module part because if it's found in * a relative import, it may be actually matched in absolute form). */ private String matchedAs; private String initialModuleName; private ImportFromModPartRenameAstEntry(ASTEntry parent, ImportFrom node, String matchedAs, String initialModuleName) { super(parent, node); this.matchedAs = matchedAs; this.initialModuleName = initialModuleName; } @Override public List<TextEdit> createRenameEdit(IDocument doc, String initialName, String inputName, RefactoringStatus status, IPath file, IPythonNature nature) { //Simple one: just the first part has to be changed. ImportFrom f = (ImportFrom) this.node; //The actual initial name String modId = ((NameTok) f.module).id; if (!(modId + ".").startsWith(initialName)) { initialName = modId; } int offset = PySelection .getAbsoluteCursorOffset(doc, f.module.beginLine - 1, f.module.beginColumn - 1); TextEditCreation.checkExpectedInput(doc, this.node.beginLine, offset, initialName, status, file); //-f.level because we'll make the import absolute now! TextEdit replaceEdit = new ReplaceEdit(offset - f.level, initialName.length() + f.level, inputName); return Arrays.asList(replaceEdit); } } private static final class ImportFromRenameAstEntry extends ImportRenameAstEntry { public Set<Integer> indexes; private ImportFromRenameAstEntry(ASTEntry parent, SimpleNode node) { super(parent, node); Assert.isTrue(node instanceof ImportFrom || node instanceof Import); } @Override public List<TextEdit> createRenameEdit(IDocument doc, String initialName, String inputName, RefactoringStatus status, IPath file, IPythonNature nature) { String line = PySelection.getLine(doc, this.node.beginLine - 1); ArrayList<TextEdit> ret = new ArrayList<>(); //Ok, this is a bit more tricky: we have a from import where we may have to change 2 parts: the from and import... //For this use case, we'll create a copy, change it, rewrite the ast and change the whole thing. stmtType importFrom = (stmtType) this.node; stmtType copied = (stmtType) importFrom.createCopy(false); //Make things from back to forward to keep indexes valid. ArrayList<Integer> sorted = new ArrayList<Integer>(indexes); Collections.sort(sorted); Collections.reverse(sorted); List<stmtType> body = new ArrayList<stmtType>(); ArrayList<aliasType> names = new ArrayList<aliasType>(); List<Import> forcedImports = new ArrayList<>(); for (int aliasIndex : indexes) { aliasType[] copiedNodeNames = getNames(copied); aliasType alias = copiedNodeNames[aliasIndex]; //Just removing the names from the copied (as it may have to be added if some other parts are //not affected). setNames(copied, ArrayUtils.remove(copiedNodeNames, aliasIndex, aliasType.class)); String full = getFull(importFrom, (NameTok) alias.name); String firstPart; //If it was an import a.b, keep it as an import (if it's dotted) boolean forceImport = importFrom instanceof Import && full.contains("."); if (forceImport) { firstPart = inputName; } else { //Otherwise, just put the last part firstPart = FullRepIterable.getLastPart(inputName); } if (full.startsWith(initialName + ".")) { NameTok t = (NameTok) alias.name; t.id = firstPart + "." + full.substring(initialName.length() + 1); } else { NameTok t = (NameTok) alias.name; t.id = firstPart; } if (forceImport) { forcedImports.add(new Import(new aliasType[] { alias })); } else { names.add(alias); } } if (forcedImports.size() > 0) { body.addAll(forcedImports); } if (names.size() > 0) { if (inputName.indexOf(".") == -1) { body.add(new Import(names.toArray(new aliasType[names.size()]))); } else { String[] headAndTail = FullRepIterable.headAndTail(inputName); NameTokType nameTok = new NameTok(headAndTail[0], NameTok.ImportModule); body.add(new ImportFrom(nameTok, names.toArray(new aliasType[names.size()]), 0)); } } if (getNames(copied).length > 0) { body.add(0, copied); } Module module = new Module(body.toArray(new stmtType[body.size()])); //We'll change all String delimiter = PySelection.getDelimiter(doc); PrettyPrinterPrefsV2 prefsV2 = PrettyPrinterV2.createDefaultPrefs(nature, DefaultIndentPrefs.get(nature), delimiter); PrettyPrinterV2 prettyPrinterV2 = new PrettyPrinterV2(prefsV2); String str = null; try { try { MakeAstValidForPrettyPrintingVisitor.makeValid(module); } catch (Exception e) { Log.log(e); } str = prettyPrinterV2.print(module); } catch (IOException e) { status.addFatalError("Unexpected exception: " + e.getMessage()); Log.log(e); } if (str != null) { str = StringUtils.rightTrim(str); Tuple<Integer, Integer> startEndOffset = NodeUtils.getStartEndOffset(doc, this.node); TextEdit replaceEdit = new ReplaceEdit(startEndOffset.o1, startEndOffset.o2 - startEndOffset.o1, str); ret.add(replaceEdit); } // System.out.println(line); // System.out.println(file); // System.out.println("ImportFromRenameAstEntry.createRenameEdit: " + initialName + " to " + inputName); // System.out.println(""); return ret; } private String getFull(stmtType imp, NameTok name) { if (imp instanceof ImportFrom) { ImportFrom importFrom = (ImportFrom) imp; return ((NameTok) importFrom.module).id + "." + name.id; } return name.id; } private void setNames(SimpleNode copied, aliasType[] arr) { if (copied instanceof ImportFrom) { ((ImportFrom) copied).names = arr; return; } if (copied instanceof Import) { ((Import) copied).names = arr; return; } throw new AssertionError("Expected Import or ImportFrom. Found: " + copied.getClass()); } private aliasType[] getNames(SimpleNode copied) { if (copied instanceof ImportFrom) { return ((ImportFrom) copied).names; } if (copied instanceof Import) { return ((Import) copied).names; } throw new AssertionError("Expected Import or ImportFrom. Found: " + copied.getClass()); } } private static final class AttributeASTEntry extends ASTEntry implements IRefactorCustomEntry { private final String fixedInitialString; private final boolean fullAttrMatch; private AttributeASTEntry(String initial, SimpleNode node, boolean fullAttrMatch) { super(null, node); this.fixedInitialString = initial; this.fullAttrMatch = fullAttrMatch; } @Override public List<TextEdit> createRenameEdit(IDocument doc, String initialName, String inputName, RefactoringStatus status, IPath file, IPythonNature nature) { initialName = fixedInitialString; if (!fullAttrMatch) { inputName = FullRepIterable.getLastPart(inputName); } int offset = AbstractRenameRefactorProcess.getOffset(doc, this); TextEditCreation.checkExpectedInput(doc, node.beginLine, offset, initialName, status, file); TextEdit replaceEdit = new ReplaceEdit(offset, initialName.length(), inputName); List<TextEdit> edits = Arrays.asList(replaceEdit); return edits; } } private IPythonNature nature; private String initialModuleName; private SourceModule currentModule; public final List<ImportFrom> importFromsMatchingOnModulePart = new ArrayList<>(); public final List<ImportFrom> importFromsMatchingOnAliasPart = new ArrayList<>(); public final List<Import> importsMatchingOnAliasPart = new ArrayList<>(); public final List<ASTEntry> occurrences = new ArrayList<>(); public final Set<String> searchStringsAs = new HashSet<>(); private ICompletionState completionState; private IProgressMonitor monitor; private String lastPart; private FastStack<SimpleNode> stack = new FastStack<>(10); public MatchImportsVisitor(IPythonNature nature, String initialName, SourceModule module, IProgressMonitor monitor) { this.nature = nature; this.initialModuleName = getWithoutInit(initialName); this.currentModule = module; completionState = CompletionStateFactory .getEmptyCompletionState(nature, new CompletionCache()); if (monitor == null) { monitor = new NullProgressMonitor(); } this.monitor = monitor; this.lastPart = FullRepIterable.getLastPart(this.initialModuleName); } protected String getModuleNameLastPart() { return lastPart; } @Override protected Object unhandled_node(SimpleNode node) throws Exception { return null; } @Override public void traverse(SimpleNode node) throws Exception { node.traverse(this); } @Override public Object visitModule(Module node) throws Exception { stack.push(node); super.visitModule(node); stack.pop(); return null; } @Override public Object visitFunctionDef(FunctionDef node) throws Exception { stack.push(node); super.visitFunctionDef(node); stack.pop(); return null; } @Override public Object visitClassDef(ClassDef node) throws Exception { stack.push(node); super.visitClassDef(node); stack.pop(); return null; } @Override public Object visitAttribute(Attribute node) throws Exception { Object ret = super.visitAttribute(node); NameTok attr = (NameTok) node.attr; if (attr.ctx == NameTok.Attrib) { if (attr.id.equals(getModuleNameLastPart())) { String checkName = NodeUtils.getFullRepresentationString(node); AttributeASTEntry entry; if (checkName.equals(this.initialModuleName)) { //The full attribute matches (i.e.: a.b) List<SimpleNode> parts = NodeUtils.getAttributeParts(node); entry = new AttributeASTEntry(checkName, parts.get(0), true); } else { //Only the last part matches (.b) entry = new AttributeASTEntry(attr.id, attr, false); } if (checkIndirectReferenceFromDefinition(checkName, true, entry, attr.beginColumn, attr.beginLine)) { return true; } } } return ret; } private boolean acceptOnlyAbsoluteImports = false; @Override public Object visitImportFrom(ImportFrom node) throws Exception { int level = node.level; String modRep = NodeUtils.getRepresentationString(node.module); if ("__future__".equals(modRep)) { if (node.names != null && node.names.length == 1) { aliasType aliasType = node.names[0]; if ("absolute_import".equals(((NameTok) aliasType.name).id)) { acceptOnlyAbsoluteImports = true; } } } HashSet<Tuple<String, Boolean>> s = new HashSet<>(); //the module and whether it's relative if (level > 0) { //Ok, direct match didn't work, so, let's check relative imports modRep = makeRelative(level, modRep); s.add(new Tuple<String, Boolean>(modRep, false)); } else { //Treat imports as relative on Python 2.x variants without the from __future__ import absolute_import statement. if (nature.getGrammarVersion() < IPythonNature.GRAMMAR_PYTHON_VERSION_3_0 && !acceptOnlyAbsoluteImports) { s.add(new Tuple<String, Boolean>(modRep, false)); s.add(new Tuple<String, Boolean>(makeRelative(1, modRep), true)); } } boolean matched = false; for (Tuple<String, Boolean> modRep2 : s) { if (!matched) { //try to check full name first matched = handleNames(node, node.names, modRep2.o1, true); } } if (!matched) { //check partial in module later for (Tuple<String, Boolean> tup : s) { if (!matched) { String modRep2 = tup.o1; boolean isRelative = tup.o2; if (modRep2.equals(this.initialModuleName) || (!isRelative && (modRep2 + ".").startsWith(initialModuleName + "."))) { //Ok, if the first part matched, no need to check other things (i.e.: rename only the from "xxx.yyy" part) importFromsMatchingOnModulePart.add(node); occurrences.add(new ImportFromModPartRenameAstEntry(null, node, modRep2, initialModuleName)); //Found a match matched = true; break; } } } } return null; } protected String makeRelative(int level, String modRep) { String parentPackage = this.currentModule.getName(); List<String> moduleParts = StringUtils.split(parentPackage, '.'); if (moduleParts.size() > level) { String relative = FullRepIterable.joinParts(moduleParts, moduleParts.size() - level); if (modRep.isEmpty()) { modRep = relative; } else { modRep = StringUtils.join(".", relative, modRep); } } return modRep; } public boolean handleNames(SimpleNode node, aliasType[] names, String modRep, boolean onlyFullMatch) { boolean handled = false; if (names != null && names.length > 0) { //not wild import! Set<Integer> aliasesHandled = new TreeSet<>(); ImportFromRenameAstEntry renameAstEntry = new ImportFromRenameAstEntry(null, node); for (int i = 0; i < names.length; i++) { aliasType aliasType = names[i]; NameTok name = (NameTok) aliasType.name; String full; final String nameInImport = name.id; if (modRep != null && modRep.length() > 0) { full = StringUtils.join(".", modRep, nameInImport); } else { full = nameInImport; } boolean addAsSearchString = aliasType.asname == null; boolean equals = full.equals(this.initialModuleName); boolean startsWith = (full + ".").startsWith(initialModuleName); if (equals || (startsWith && !onlyFullMatch)) { //Ok, this match is a bit more tricky: we matched it, but we need to rename a part before and after the from xxx.yyy import zzz part //also, we must take care not to destroy any alias in the process or other imports which may be joined with this one (the easiest part //is probably removing the whole import and re-writing everything again). if (node instanceof ImportFrom) { importFromsMatchingOnAliasPart.add((ImportFrom) node); aliasesHandled.add(i); if (addAsSearchString) { searchStringsAs.add(nameInImport); } } else if (node instanceof Import) { importsMatchingOnAliasPart.add((Import) node); aliasesHandled.add(i); if (addAsSearchString) { searchStringsAs.add(nameInImport); } } if (aliasType.asname == null) { boolean forceFull = node instanceof Import && startsWith && full.contains("."); String checkName = forceFull ? initialModuleName : nameInImport; findOccurrences(forceFull, checkName); } handled = true; } else { if (nameInImport.equals(getModuleNameLastPart())) { if (checkIndirectReferenceFromDefinition(nameInImport, addAsSearchString, renameAstEntry, node.beginColumn, node.beginLine)) { findOccurrences(false, nameInImport); aliasesHandled.add(i); handled = true; } } } } if (aliasesHandled.size() > 0) { renameAstEntry.indexes = aliasesHandled; occurrences.add(renameAstEntry); } } return handled; } protected void findOccurrences(boolean forceFull, String checkName) { List<ASTEntry> localOccurrences = ScopeAnalysis.getLocalOccurrences(checkName, stack.peek()); for (ASTEntry astEntry : localOccurrences) { if ((astEntry.node instanceof NameTok) && (((NameTok) astEntry.node).ctx == NameTok.ImportName || ((NameTok) astEntry.node).ctx == NameTok.ImportModule)) { //i.e.: skip if it's an import as we already handle those! continue; } else { occurrences.add(new PyRenameImportProcess.FixedInputStringASTEntry(checkName, null, astEntry.node, forceFull)); } } } protected boolean checkIndirectReferenceFromDefinition(String nameInImport, boolean addAsSearchString, ASTEntry renameAstEntry, int beginColumn, int beginLine) { ArrayList<IDefinition> definitions = new ArrayList<>(); try { PyRefactoringFindDefinition.findActualDefinition(monitor, this.currentModule, nameInImport, definitions, beginLine, beginColumn, nature, this.completionState); for (IDefinition iDefinition : definitions) { String modName = getWithoutInit(iDefinition.getModule().getName()); if (modName.equals(this.initialModuleName)) { occurrences.add(renameAstEntry); if (addAsSearchString) { searchStringsAs.add(nameInImport); } return true; } } } catch (CompletionRecursionException e) { Log.log(e); } catch (Exception e) { Log.log(e); } return false; } private String getWithoutInit(String initialName) { if (initialName.endsWith(".__init__")) { initialName = initialName.substring(0, initialName.length() - 9); } return initialName; } @Override public Object visitImport(Import node) throws Exception { aliasType[] names = node.names; boolean matched = handleNames(node, names, "", false); //Treat imports as relative on Python 2.x variants without the from __future__ import absolute_import statement. if (!matched && nature.getGrammarVersion() < IPythonNature.GRAMMAR_PYTHON_VERSION_3_0) { String relative = makeRelative(1, ""); handleNames(node, names, relative, true); } return null; } public List<ASTEntry> getEntryOccurrences() { return new ArrayList<ASTEntry>(occurrences); } }