/** * Copyright (c) 2005-2013 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Eclipse Public License (EPL). * Please see the license.txt included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package com.python.pydev.refactoring.tdd; import java.io.File; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import org.eclipse.core.resources.IMarker; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.TextSelection; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.swt.graphics.Image; import org.python.pydev.core.FullRepIterable; import org.python.pydev.core.ICodeCompletionASTManager; import org.python.pydev.core.ICompletionState; import org.python.pydev.core.IDefinition; import org.python.pydev.core.IModule; import org.python.pydev.core.IPythonNature; import org.python.pydev.core.docutils.ImportHandle; import org.python.pydev.core.docutils.ImportHandle.ImportHandleInfo; import org.python.pydev.core.docutils.PyImportsHandling; 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.PyEdit; import org.python.pydev.editor.codecompletion.revisited.CompletionCache; import org.python.pydev.editor.codecompletion.revisited.CompletionStateFactory; import org.python.pydev.editor.codecompletion.revisited.PythonPathHelper; import org.python.pydev.editor.codecompletion.revisited.modules.SourceModule; import org.python.pydev.editor.codecompletion.revisited.visitors.Definition; import org.python.pydev.editor.codefolding.MarkerAnnotationAndPosition; import org.python.pydev.editor.refactoring.PyRefactoringFindDefinition; import org.python.pydev.editor.refactoring.RefactoringRequest; import org.python.pydev.parser.jython.ast.ClassDef; import org.python.pydev.parser.visitors.NodeUtils; import org.python.pydev.plugin.PydevPlugin; import org.python.pydev.shared_core.io.FileUtils; import org.python.pydev.shared_core.string.StringUtils; import org.python.pydev.shared_core.structure.Tuple; import org.python.pydev.shared_ui.ImageCache; import org.python.pydev.shared_ui.UIConstants; import org.python.pydev.shared_ui.proposals.IPyCompletionProposal; import org.python.pydev.ui.filetypes.FileTypesPreferencesPage; import com.python.pydev.analysis.IAnalysisPreferences; import com.python.pydev.analysis.builder.AnalysisRunner; import com.python.pydev.analysis.ctrl_1.IAnalysisMarkersParticipant; /** * This participant will add a suggestion to create class/methods/attributes when an undefined variable error is found. */ public class TddQuickFixParticipant implements IAnalysisMarkersParticipant { /*default*/Image imageClass; /*default*/Image imageMethod; /*default*/Image imageModule; public TddQuickFixParticipant() { ImageCache imageCache = PydevPlugin.getImageCache(); if (imageCache != null) { //making tests imageClass = imageCache.get(UIConstants.CREATE_CLASS_ICON); imageMethod = imageCache.get(UIConstants.CREATE_METHOD_ICON); imageModule = imageCache.get(UIConstants.CREATE_MODULE_ICON); } } @Override public void addProps(MarkerAnnotationAndPosition markerAnnotation, IAnalysisPreferences analysisPreferences, String line, PySelection ps, int offset, IPythonNature nature, PyEdit edit, List<ICompletionProposal> props) throws BadLocationException, CoreException { if (nature == null) { return; } ICodeCompletionASTManager astManager = nature.getAstManager(); if (astManager == null) { return; } if (markerAnnotation.position == null) { return; } IMarker marker = markerAnnotation.markerAnnotation.getMarker(); Integer id = (Integer) marker.getAttribute(AnalysisRunner.PYDEV_ANALYSIS_TYPE); int start = markerAnnotation.position.offset; int end = start + markerAnnotation.position.length; ps.setSelection(start, end); String markerContents; try { markerContents = ps.getSelectedText(); } catch (Exception e1) { return; //Selection may be wrong. } IDocument doc = ps.getDoc(); List<String> parametersAfterCall = ps.getParametersAfterCall(end); switch (id) { case IAnalysisPreferences.TYPE_UNDEFINED_VARIABLE: addCreateClassOption(ps, edit, props, markerContents, parametersAfterCall); addCreateMethodOption(ps, edit, props, markerContents, parametersAfterCall); break; case IAnalysisPreferences.TYPE_UNDEFINED_IMPORT_VARIABLE: //Say we had something as: //import sys //sys.Bar //in which case 'Bar' is undefined //in this situation, the activationTokenAndQual would be "sys." and "Bar" //and we want to get the definition for "sys" String[] activationTokenAndQual = ps.getActivationTokenAndQual(true); if (activationTokenAndQual[0].endsWith(".")) { ArrayList<IDefinition> selected = findDefinitions(nature, edit, start - 2, doc); for (IDefinition iDefinition : selected) { IModule module = iDefinition.getModule(); if (module.getFile() != null) { Definition definition = (Definition) iDefinition; File file = module.getFile(); if (definition.ast == null) { //if we have no ast in the definition, it means the module itself was found (global scope) //Add option to create class at the given module! addCreateClassOption(ps, edit, props, markerContents, parametersAfterCall, file); addCreateMethodOption(ps, edit, props, markerContents, parametersAfterCall, file); } else if (definition.ast instanceof ClassDef) { ClassDef classDef = (ClassDef) definition.ast; //Ok, we should create a field or method in this case (accessing a classmethod or staticmethod) PyCreateMethodOrField pyCreateMethod = new PyCreateMethodOrField(); String className = NodeUtils.getNameFromNameTok(classDef.name); pyCreateMethod.setCreateInClass(className); pyCreateMethod.setCreateAs(PyCreateMethodOrField.CLASSMETHOD); addCreateClassmethodOption(ps, edit, props, markerContents, parametersAfterCall, pyCreateMethod, file, className); } } } } break; case IAnalysisPreferences.TYPE_UNRESOLVED_IMPORT: //This case is the following: from other_module4 import Foo //with 'Foo' being undefined. //So, we have to suggest creating a Foo class/method in other_module4 PyImportsHandling importsHandling = new PyImportsHandling(ps.getDoc(), false); int offsetLine = ps.getLineOfOffset(start); String selectedText = ps.getSelectedText(); Tuple<IModule, String> found = null; String foundFromImportStr = null; boolean isImportFrom = false; OUT: for (ImportHandle handle : importsHandling) { if (handle.startFoundLine == offsetLine || handle.endFoundLine == offsetLine || (handle.startFoundLine < offsetLine && handle.endFoundLine > offsetLine)) { List<ImportHandleInfo> importInfo = handle.getImportInfo(); for (ImportHandleInfo importHandleInfo : importInfo) { String fromImportStr = importHandleInfo.getFromImportStr(); List<String> importedStr = importHandleInfo.getImportedStr(); for (String imported : importedStr) { if (selectedText.equals(imported)) { if (fromImportStr != null) { foundFromImportStr = fromImportStr + "." + imported; isImportFrom = true; } else { //if fromImportStr == null, it's not a from xxx import yyy (i.e.: simple import) foundFromImportStr = imported; } try { String currentModule = nature.resolveModule(edit.getEditorFile()); ICompletionState state = CompletionStateFactory.getEmptyCompletionState(nature, new CompletionCache()); found = nature.getAstManager().findModule( foundFromImportStr, currentModule, state, new SourceModule(currentModule, edit.getEditorFile(), edit.getAST(), null, nature)); } catch (Exception e) { Log.log(e); } break OUT; } } } break OUT; } } boolean addOptionToCreateClassOrMethod = isImportFrom; if (found != null && found.o1 != null) { //Ok, we found a module, now, it may be that we still have to create some intermediary modules //or just create a class or method at the end. if (found.o1 instanceof SourceModule) { //if all was found, there's nothing left to create. if (found.o2 != null && found.o2.length() > 0) { SourceModule sourceModule = (SourceModule) found.o1; File file = sourceModule.getFile(); if (found.o2.indexOf('.') != -1) { //We have to create some intermediary structure. if (!addOptionToCreateClassOrMethod) { //Cannot create class or method from the info (only the module structure). if (sourceModule.getName().endsWith(".__init__")) { File f = getFileStructure(file.getParentFile(), found.o2); addCreateModuleOption(ps, edit, props, markerContents, f); } } else { //Ok, the leaf may be a class or method. if (sourceModule.getName().endsWith(".__init__")) { String moduleName = FullRepIterable.getWithoutLastPart(sourceModule.getName()); String withoutLastPart = FullRepIterable.getWithoutLastPart(found.o2); moduleName += "." + withoutLastPart; String classOrMethodName = FullRepIterable.getLastPart(found.o2); File f = getFileStructure(file.getParentFile(), withoutLastPart); addCreateClassInNewModuleOption(ps, edit, props, classOrMethodName, moduleName, parametersAfterCall, f); addCreateMethodInNewModuleOption(ps, edit, props, classOrMethodName, moduleName, parametersAfterCall, f); } } } else { //Ok, it's all there, we just have to create the leaf. if (!addOptionToCreateClassOrMethod || sourceModule.getName().endsWith(".__init__")) { //Cannot create class or method from the info (only the module structure). if (sourceModule.getName().endsWith(".__init__")) { File f = new File(file.getParent(), found.o2 + FileTypesPreferencesPage.getDefaultDottedPythonExtension()); addCreateModuleOption(ps, edit, props, markerContents, f); } } else { //Ok, the leaf may be a class or method. addCreateClassOption(ps, edit, props, markerContents, parametersAfterCall, file); addCreateMethodOption(ps, edit, props, markerContents, parametersAfterCall, file); } } } } } else if (foundFromImportStr != null) { //We couldn't find anything there, so, we have to create the modules structure as needed and //maybe create a class or module at the end (but only if it's an import from). //Ok, it's all there, we just have to create the leaf. //Discover the source folder where we should create the structure. File editorFile = edit.getEditorFile(); String onlyProjectPythonPathStr = nature.getPythonPathNature().getOnlyProjectPythonPathStr(false); List<String> split = StringUtils.splitAndRemoveEmptyTrimmed(onlyProjectPythonPathStr, '|'); for (int i = 0; i < split.size(); i++) { String fullPath = FileUtils.getFileAbsolutePath(split.get(i)); fullPath = PythonPathHelper.getDefaultPathStr(fullPath); split.set(i, fullPath); } HashSet<String> projectSourcePath = new HashSet<String>(split); if (projectSourcePath.size() == 0) { return; //No source folder for editor... this shouldn't happen (code analysis wouldn't even run on it). } String fullPath = FileUtils.getFileAbsolutePath(editorFile); fullPath = PythonPathHelper.getDefaultPathStr(fullPath); String foundSourceFolderFullPath = null; if (projectSourcePath.size() == 1) { foundSourceFolderFullPath = projectSourcePath.iterator().next(); } else { for (String string : projectSourcePath) { if (fullPath.startsWith(string)) { //Use this as the source folder foundSourceFolderFullPath = string; break; } } } if (foundSourceFolderFullPath != null) { if (!addOptionToCreateClassOrMethod) { //Cannot create class or method from the info (only the module structure). File f = getFileStructure(new File(foundSourceFolderFullPath), foundFromImportStr); addCreateModuleOption(ps, edit, props, foundFromImportStr, f); } else { //Ok, the leaf may be a class or method. String moduleName = FullRepIterable.getWithoutLastPart(foundFromImportStr); File file = getFileStructure(new File(foundSourceFolderFullPath), moduleName); String lastPart = FullRepIterable.getLastPart(foundFromImportStr); addCreateClassInNewModuleOption(ps, edit, props, lastPart, moduleName, parametersAfterCall, file); addCreateMethodInNewModuleOption(ps, edit, props, lastPart, moduleName, parametersAfterCall, file); } } } break; } } protected File getFileStructure(File file, String withoutLastPart) { File f = file; List<String> split = StringUtils.dotSplit(withoutLastPart); int size = split.size(); for (int i = 0; i < size; i++) { if (i == size - 1) { f = new File(f, split.get(i) + FileTypesPreferencesPage.getDefaultDottedPythonExtension()); } else { f = new File(f, split.get(i)); } } return f; } private void addCreateClassmethodOption(PySelection ps, PyEdit edit, List<ICompletionProposal> props, String markerContents, List<String> parametersAfterCall, PyCreateMethodOrField pyCreateMethod, File file, String className) { props.add(new TddRefactorCompletionInModule(markerContents, imageMethod, "Create " + markerContents + " classmethod at " + className + " in " + file.getName(), null, "Create " + markerContents + " classmethod at class: " + className + " in " + file, IPyCompletionProposal.PRIORITY_CREATE, edit, file, parametersAfterCall, pyCreateMethod, ps)); } private void addCreateMethodOption(PySelection ps, PyEdit edit, List<ICompletionProposal> props, String markerContents, List<String> parametersAfterCall, File file) { props.add(new TddRefactorCompletionInModule(markerContents, imageMethod, "Create " + markerContents + " method at " + file.getName(), null, "Create " + markerContents + " method at " + file, IPyCompletionProposal.PRIORITY_CREATE, edit, file, parametersAfterCall, new PyCreateMethodOrField(), ps)); } private void addCreateClassOption(PySelection ps, PyEdit edit, List<ICompletionProposal> props, String markerContents, List<String> parametersAfterCall, File file) { props.add(new TddRefactorCompletionInModule(markerContents, imageClass, "Create " + markerContents + " class at " + file.getName(), null, "Create " + markerContents + " class at " + file, IPyCompletionProposal.PRIORITY_CREATE, edit, file, parametersAfterCall, new PyCreateClass(), ps)); } private void addCreateClassInNewModuleOption(PySelection ps, PyEdit edit, List<ICompletionProposal> props, String markerContents, String moduleName, List<String> parametersAfterCall, File file) { props.add(new TddRefactorCompletionInInexistentModule(markerContents, imageClass, "Create " + markerContents + " class at new module " + moduleName, null, "Create " + markerContents + " class at new module " + file, IPyCompletionProposal.PRIORITY_CREATE, edit, file, new ArrayList<String>(), new PyCreateClass(), ps)); } private void addCreateMethodInNewModuleOption(PySelection ps, PyEdit edit, List<ICompletionProposal> props, String markerContents, String moduleName, List<String> parametersAfterCall, File file) { props.add(new TddRefactorCompletionInInexistentModule(markerContents, imageMethod, "Create " + markerContents + " method at new module " + moduleName, null, "Create " + markerContents + " method at new module " + file, IPyCompletionProposal.PRIORITY_CREATE, edit, file, new ArrayList<String>(), new PyCreateMethodOrField(), ps)); } private void addCreateModuleOption(PySelection ps, PyEdit edit, List<ICompletionProposal> props, String markerContents, File file) { props.add(new TddRefactorCompletionInInexistentModule(markerContents, imageModule, "Create " + markerContents + " module", null, "Create " + markerContents + " module (" + file + ")", IPyCompletionProposal.PRIORITY_CREATE, edit, file, new ArrayList<String>(), new NullPyCreateAction(), ps)); } private void addCreateMethodOption(PySelection ps, PyEdit edit, List<ICompletionProposal> props, String markerContents, List<String> parametersAfterCall) { props.add(new TddRefactorCompletion(markerContents, imageMethod, "Create " + markerContents + " method", null, null, IPyCompletionProposal.PRIORITY_CREATE, edit, PyCreateClass.LOCATION_STRATEGY_BEFORE_CURRENT, parametersAfterCall, new PyCreateMethodOrField(), ps)); } private void addCreateClassOption(PySelection ps, PyEdit edit, List<ICompletionProposal> props, String markerContents, List<String> parametersAfterCall) { props.add(new TddRefactorCompletion(markerContents, imageClass, "Create " + markerContents + " class", null, null, IPyCompletionProposal.PRIORITY_CREATE, edit, PyCreateClass.LOCATION_STRATEGY_BEFORE_CURRENT, parametersAfterCall, new PyCreateClass(), ps)); } private ArrayList<IDefinition> findDefinitions(IPythonNature nature, PyEdit edit, int start, IDocument doc) { CompletionCache completionCache = new CompletionCache(); ArrayList<IDefinition> selected = new ArrayList<IDefinition>(); RefactoringRequest request = new RefactoringRequest(edit.getEditorFile(), new PySelection(doc, new TextSelection(doc, start, 0)), new NullProgressMonitor(), nature, edit); try { PyRefactoringFindDefinition.findActualDefinition(request, completionCache, selected); } catch (CompletionRecursionException | BadLocationException e1) { Log.log(e1); } return selected; } }