PyDev Refactoring Beispiel
Im Folgenden soll ein kleines Beispiel für ein Refactoring in PyDev gegeben werden. Das Beispiel ist als externes Plugin realisiert. Da etliche Pakete aus den PyDev-plugins nicht exportiert werden, kann auf Teile der Funktionalität von PyDev nicht zugegriffen werden. Änderungen an PyDev selbst scheinen mir jedoch schon deshalb nicht einfach, weil es reichlich Fehlermeldungen nach dem Import der PyDev-Plugin-Projekte gibt (siehe Installation).
Inhaltsverzeichnis
Das Insert-None-Refactoring
Ziel dieses Refactoring ist es, zu erkennen, ob der Cursor auf einem Namen steht und, wenn ja, in der Zeile davor ein neues Statement einzufügen, das diesem None
zuweist.
Diese Zuweisung ist nicht besonders nützlich, aber es handelt sich ja auch nur um ein Beispiel.
Das Beispiel ist nicht perfekt und es ist wahrscheinlich möglich, durch Anwendung des Refactorings fehlerhaften Code zu erzeugen, siehe #Probleme.
Um das Beispiel auszuprobieren, können das Eclipse-Projekt und das fertige Plugin heruntergeladen werden.
InsertNoneRefactoring
Diese Klasse übernimmt das eigentliche Refactoring.
Wie mit dem Eclipse LTK üblich und wie in Eclipse Refactoring Framework beschrieben, werden zunächst checkInitialConditions(...)
und
checkFinalConditions(...)
aufgerufen und danach in createChange
die Änderungen berechnet.
Der wichtigste Teil ist checkInitialConditions(...)
.
Hier wird mithilfe eines InsertNoneNameFinderVisitor
der AST des Editors durchlaufen, um herauszufinden, ob der Cursor im Editor gerade auf einem Namen und dieser im passenden Kontext steht.
package feu.k01919.q7649347.pyrefactor.insertnone; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.ltk.core.refactoring.Change; import org.eclipse.ltk.core.refactoring.Refactoring; import org.eclipse.ltk.core.refactoring.RefactoringStatus; import org.eclipse.ltk.core.refactoring.TextChange; import org.eclipse.text.edits.InsertEdit; import org.python.pydev.core.docutils.PySelection; import org.python.pydev.editor.PyEdit; import org.python.pydev.parser.jython.SimpleNode; import org.python.pydev.parser.jython.ast.Name; import org.python.pydev.parser.visitors.scope.GetNodeForExtractLocalVisitor; import org.python.pydev.refactoring.core.base.PyDocumentChange; import org.python.pydev.refactoring.core.base.PyTextFileChange; import org.python.pydev.refactoring.core.base.RefactoringInfo; /** * A refactoring assigning "None" to the name at the selection start. * @author Alexander Bürger <acfbuerger@googlemail.com> */ public class InsertNoneRefactoring extends Refactoring { private PyEdit editor; private RefactoringInfo info; private Name name; public InsertNoneRefactoring(PyEdit editor, RefactoringInfo info) { this.editor = editor; this.info = info; } @Override public RefactoringStatus checkInitialConditions(IProgressMonitor pm) throws CoreException, OperationCanceledException { RefactoringStatus status = new RefactoringStatus(); // walk the complete AST to find out the name we sit on InsertNoneNameFinderVisitor nsv = new InsertNoneNameFinderVisitor(info.getDocument(), info.getUserSelection()); try { // info.getModuleAdapter().getASTNode().accept(nsv); prohibited by access restriction editor.getAST().accept(nsv); } catch (Exception e) { status.addFatalError("AST Exception"); } name = nsv.getName(); if( name == null ) status.addFatalError("No Name found at selection start."); return status; } @Override public RefactoringStatus checkFinalConditions(IProgressMonitor pm) throws CoreException, OperationCanceledException { // TODO check final conditions return new RefactoringStatus(); } @Override public Change createChange(IProgressMonitor pm) throws CoreException, OperationCanceledException { // figure out the offset in the document where we have to insert our new // statement; based on CreateLocalVariableEdit.calculateLineForLocal PySelection ps = new PySelection(info.getDocument(), info.getUserSelection()); GetNodeForExtractLocalVisitor visitor = new GetNodeForExtractLocalVisitor(name.beginLine); try { editor.getAST().accept(visitor); } catch (Exception e) { throw new RuntimeException(e); } SimpleNode lastNodeBeforePassedLine = visitor.getLastInContextBeforePassedLine(); int insertLine; if (lastNodeBeforePassedLine != null) { insertLine = lastNodeBeforePassedLine.beginLine-1; } else { insertLine = ps.getStartLineIndex(); } int insertOffset = ps.getLineOffset(insertLine); // use same indentation as the line where we want to insert our statement PySelection psi = new PySelection(info.getDocument(), insertLine, 0); String indent = psi.getIndentationFromLine(); // this is based on AbstractFileChangeProcessor.createChange() TextChange tc; if (info.getSourceFile() != null) { tc = new PyTextFileChange(getName(), info.getSourceFile()); } else { tc = new PyDocumentChange(getName(), info.getDocument()); } // insert a line assigning None to the selected name tc.setEdit(new InsertEdit(insertOffset, indent + name.id + " = None" + info.getNewLineDelim())); return tc; } @Override public String getName() { return "InsertNone Refactoring"; } }
InsertNoneNameFinderVisitor
Dieser Visitor ist dazu gedacht, den AST eines PyEdit
ors zu durchlaufen.
Dabei wird untersucht, ob ein Name an der Stelle steht, an der die Auswahl im Editor beginnt — das kann auch die Cursorposition sein.
Das Ende der Auswahl wird nicht beachtet.
Nicht immer ist es sinnvoll, in der Zeile vor dem Auftreten eines Namens eine Zuweisung zu diesem Namen durchzuführen. Zwei dieser Fälle (gibt es mehr?) werden hier gesondert behandelt:
- Namen in
global
statements werden ignoriert, denn davor ist der Scope wahrscheinlich falsch und die Zuweisung wäre ein Programmfehler. - Namen der Argumente von Funktions-Definitionen werden ebenfalls ignoriert. Auch hier ist es höchstwahrscheinlich ein Fehler, eine Zuweisung einzufügen.
package feu.k01919.q7649347.pyrefactor.insertnone; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.ITextSelection; import org.python.pydev.core.docutils.PySelection; import org.python.pydev.parser.jython.SimpleNode; import org.python.pydev.parser.jython.ast.FunctionDef; import org.python.pydev.parser.jython.ast.Global; import org.python.pydev.parser.jython.ast.Import; import org.python.pydev.parser.jython.ast.ImportFrom; import org.python.pydev.parser.jython.ast.Name; import org.python.pydev.parser.jython.ast.VisitorBase; import org.python.pydev.parser.jython.ast.stmtType; /** * A visitor searching for a name at the selection start. * @author Alexander Bürger <acfbuerger@googlemail.com> */ public class InsertNoneNameFinderVisitor extends VisitorBase { private IDocument document; private ITextSelection selection; private Name name; public InsertNoneNameFinderVisitor(IDocument doc, ITextSelection s) { document = doc; selection = s; } public Object visitImport(Import node) throws Exception { // we are not interested in imports return null; } @Override public Object visitImportFrom(ImportFrom node) throws Exception { // we are not interested in imports return null; } @Override public Object visitGlobal(Global node) throws Exception { // we are not interested in global declarations return null; } @Override public Object visitFunctionDef(FunctionDef node) throws Exception { // for function definitions, we do not care about the arguments -- it // does not make sense to insert an assignment before a function definition if (node.body != null) { for (stmtType b: node.body) { if (b != null) b.accept(this); } } return null; } @Override public Object visitName(Name node) throws Exception { // check if the start of the selection is within the name if( selection.getStartLine() == node.beginLine -1 ) { int selectionOffset = selection.getOffset(); int begin = PySelection.getAbsoluteCursorOffset(document, node.beginLine - 1, node.beginColumn - 1); int end = begin + node.id.length(); if (selectionOffset >= begin && selectionOffset <= end) return name = node; } return super.visitName(node); } /** * Retrieve the name we found. * @return the name, might be null */ public Name getName() { return name; } @Override public void traverse(SimpleNode node) throws Exception { node.traverse(this); } @Override protected Object unhandled_node(SimpleNode node) throws Exception { return null; } }
InsertNoneWizard
Diese Klasse implementiert den Dialog mit der Vorschau der Änderungen, die das Refactoring durchführen wird/würde. Da keine Eingaben des Benutzers zu verarbeiten sind, wird nur der Unterschied zwischen dem Original und dem geänderten Quelltext gezeigt.
package feu.k01919.q7649347.pyrefactor.insertnone; import org.eclipse.ltk.ui.refactoring.RefactoringWizard; /** * An almost empty wizard. It only whos the diff page. * @author Alexander Bürger <acfbuerger@googlemail.com> */ public class InsertNoneWizard extends RefactoringWizard { public InsertNoneWizard(InsertNoneRefactoring inr) { super(inr, DIALOG_BASED_USER_INTERFACE | CHECK_INITIAL_CONDITIONS_ON_OPEN | PREVIEW_EXPAND_FIRST_NODE | NO_BACK_BUTTON_ON_STATUS_DIALOG ); setDefaultPageTitle(inr.getName()); } @Override protected void addUserInputPages() { // no need for user input } }
PyInsertNoneAction
Diese Klasse implementiert den ActionDelegate
, der vom Benutzer aufgerufen werden kann.
package feu.k01919.q7649347.pyrefactor.actions; import org.eclipse.core.runtime.Status; import org.eclipse.jface.action.IAction; import org.eclipse.jface.dialogs.ErrorDialog; import org.eclipse.jface.viewers.ISelection; import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IEditorActionDelegate; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; import org.python.pydev.core.MisconfigurationException; import org.python.pydev.editor.PyEdit; import org.python.pydev.refactoring.core.base.RefactoringInfo; import feu.k01919.q7649347.pyrefactor.Activator; import feu.k01919.q7649347.pyrefactor.insertnone.InsertNoneRefactoring; import feu.k01919.q7649347.pyrefactor.insertnone.InsertNoneWizard; /** * Editor action for Python refactoring example. * @see IEditorActionDelegate * @author Alexander Bürger <acfbuerger@googlemail.com> */ public class PyInsertNoneAction implements IEditorActionDelegate { private volatile PyEdit editor; /** * @see IEditorActionDelegate#run */ public void run(IAction action) { if( editor == null ) { openError("Editor is not a PyDev PyEdit"); return; } RefactoringInfo ri; try { ri = new RefactoringInfo(editor); } catch (MisconfigurationException e) { openError("PyDev Misconfiguration"); return; } InsertNoneRefactoring inr = new InsertNoneRefactoring(editor, ri); InsertNoneWizard wizard = new InsertNoneWizard(inr); RefactoringWizardOpenOperation rwoo = new RefactoringWizardOpenOperation(wizard); try { rwoo.run(getShell(), inr.getName()); } catch (InterruptedException e) { } } private Shell getShell() { IWorkbench workbench = PlatformUI.getWorkbench(); IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); return window.getShell(); } private void openError(String message) { Status status = new Status(Status.ERROR, Activator.PLUGIN_ID, message); ErrorDialog.openError(getShell(), "PyInsertNone not possible.", message, status); } @Override public void selectionChanged(IAction action, ISelection selection) { // we do not care } @Override public void setActiveEditor(IAction action, IEditorPart editor) { if (editor instanceof PyEdit) this.editor = (PyEdit)editor; else this.editor = null; } }
InsertNone in Aktion
Zur Demonstraion des InsertNoneRefactoring
wird nun folgendes Python-Skript beispiel.py
verwendet:
import sys def meine_funktion(x, y): return x + 5*y a = 5 vektor = [a, meine_funktion(a, -2), 12] if len(sys.argv)>1: print meine_funktion(float(sys.argv[1]), -1) else: x = 17 print x
Das Plugin wurde dabei in einer Debugging-Workbench gestartet.
meine_funktion
Hier soll vor Zeile 13 das Statement
meine_funktion = None
eingefügt werden. Dazu wird der Cursor in Zeile 13 auf meine_funktion
gesetzt und im Menü PyRefactor > Python Insert None aufgerufen.
Es ergibt sich folgendes Bild:
Vor dem print
-Statement wird also eine Zuweisung eingefügt.
mehrzeilige Zuweisung
Hier soll vor Zeile 9 das Statement
a = None
eingefügt werden. Dazu wird der Cursor in Zeile 9 auf a
gesetzt und im Menü PyRefactor > Python Insert None aufgerufen.
Es ergibt sich folgendes Bild:
Dank des GetNodeForExtractLocalVisitor
wird die richtige Zeile vor der mehrzeiligen Zuweisung gefunden und die Zuweisung dort eingefügt.
Parameter
Hier soll geprüft werden, dass vor Zeile 4 kein Statement
x = None
eingefügt wird. Dazu wird der Cursor in Zeile 4 auf x
gesetzt und im Menü PyRefactor > Python Insert None aufgerufen.
Es ergibt sich folgendes Bild:
Der InsertNoneNameFinderVisitor
stellt fest, dass es sich um Parameter eines Funktions-Definition handelt und es gibt eine Fehlermeldung.
Probleme
Beispielsweise bei folgender Situation versagt der GetNodeForExtractLocalVisitor
:
def fun(a,b): c = a; return 5*c+b
Wird hier der Ausdruck 5*ac+b
selektiert und das PyDev-Refactoring Extract Local Variable aufgerufen, wird beispielsweise folgender Code erzeugt:
def fun(a,b): x = 5*c+b c = a; return x
Dieser Code ist fehlerhaft, denn c
ist noch nicht definiert und kann nicht gelesen werden.