PyDev AST Viewer

Aus Eclipse
Version vom 15. Juli 2010, 09:02 Uhr von Thies (Diskussion | Beiträge)

(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Wechseln zu: Navigation, Suche

Überblick

Um den AST besser überblicken zu können, wurde im Rahmen dieser Seminararbeit ein einfacher AST Viewer für PyDev entwickelt. Er ist im Eclipse-Projekt zu dieser Seminararbeit (und fertig compiliert im entsprechenden Plugin) enthalten. Der Viewer lässt sich (nach Installation von PyDev und des Plugin zu dieser Seminararbeit) mit Window > Show View > Other... > PyDev AST einblenden. Der dargestellte AST wird automatisch immer dann ersetzt, wenn der Parser zum Python-Editor einen neuen Baum aufgebaut hat.

Bei Doppelklick auf einen der (bunten) Knoten im AST-Viewer wird der Cursor im Editor an die Stelle gesetzt, die der AST-Knoten als Startpunkt verzeichnet. Diese Stellen sind manchmal überraschend — beispielsweise ist der Startpunkt für ein Print der Beginn der Argumente der print-Anweisung.

PyDev AST Viewer 1.png

Im Viewer sind die Knoten, die ein Visitor sehen würde, mit einem grünen Punkt markiert. Um zu bestimmen, welche Knoten keinen grünen Punkt bekommen, wird allerdings kein Visitor verwendet, sondern der Klassen-Name untersucht.

Zum AST Viewer gibt es eine Hilfe-Funktion, die anhand der Anleitung im Seminarbeitrag Dokumentation und Hilfe erstellt wurde.

Implementierung

Der Aufbau des Baums im Viewer erfolgt durch Reflection.

Die Klassen der Knoten des PyDev AST werden vom Skript asdl_java.py erzeugt und sind daher alle gleich aufgebaut. Insbesondere erhalten alle Klassen mit Namen xyType eine Methode accept(VisitorIF), die nur die jeweiligen Kinderknoten besucht, ohne direkt den Visitor aufzurufen. Außerdem sind alle Attribute der Knoten als public Fields implementiert.

ASTView

Die Klasse ASTView definiert den View, in dem der AST als Baum angezeigt wird.

package feu.k01919.q7649347.pyrefactor.astview;
 
import java.util.Iterator;
 
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.TreeViewerColumn;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.part.ViewPart;
import org.python.pydev.core.parser.IParserObserver;
import org.python.pydev.core.parser.ISimpleNode;
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.jython.ast.NameTok;
import org.python.pydev.parser.jython.ast.Num;
 
import feu.k01919.q7649347.pyrefactor.Activator;
 
/**
 * A viewer for the AST in PyDev.
 * 
 * @author Alexander Bürger <acfbuerger@googlemail.com>
 */
public class ASTView extends ViewPart {
 
    /**
     * The view's id.
     */
    public static final String ID = "feu.k01919.q7649347.pyrefactor.astview.ASTView";
 
    public static final String CONTEXT_HELP_ID = Activator.PLUGIN_ID + ".ASTView";
 
    /**
     * The tree (JFace) widget.
     */
    private TreeViewer viewer;
 
    /**
     * The parser listener attached to the PyEdit.
     */
    private IParserObserver parserObserver;
 
    /**
     * The listener for editor changes.
     */
    private ISelectionListener pageSelectionListener;
 
    /**
     * The present editor, if it is a PyEdit.
     */
    private PyEdit editor;
 
    /**
     * The last time the editor's parser has finished parsing. Used to decide
     * when to rebuild the tree.
     */
    private long astTimeStamp;
 
    /**
     * Create the SWT/JFace widgets.
     */
    public void createPartControl(Composite parent) {
        viewer = new TreeViewer(parent, SWT.FULL_SELECTION | SWT.H_SCROLL | SWT.V_SCROLL);
        viewer.getTree().setLinesVisible(true);
        viewer.getTree().setHeaderVisible(true);
 
        viewer.setContentProvider(new ASTContentProvider());
 
        Activator.getDefault().getWorkbench().getHelpSystem().setHelp(viewer.getTree(), CONTEXT_HELP_ID);
 
        TreeViewerColumn colName = new TreeViewerColumn(viewer, SWT.LEFT);
        colName.getColumn().setWidth(200);
        colName.getColumn().setText("Name");
        colName.setLabelProvider(new NameLabelProvider());
 
        TreeViewerColumn colValue = new TreeViewerColumn(viewer, SWT.LEFT);
        colValue.getColumn().setWidth(80);
        colValue.getColumn().setText("Value");
        colValue.setLabelProvider(new ValueLabelProvider());
 
        hookDoubleClickCommand();
        hookPageSelection();
        clearASTView();
    }
 
    /**
     * Called if a different editor is selected. Re-builds the AST Viewer tree
     * if applicable.
     */
    private void pageSelectionChanged(IWorkbenchPart part, ISelection selection) {
        if (part == this)
            return;
        boolean isPyEditPart = (part != null && part instanceof PyEdit);
        boolean isDifferentEditor = (isPyEditPart && part != editor);
        if (!isPyEditPart || isDifferentEditor) {
            if (editor != null && parserObserver != null)
                editor.getParser().removeParseListener(parserObserver);
            clearASTView();
            editor = null;
        }
        if (isDifferentEditor) {
            editor = (PyEdit) part;
 
            // reset timestamp, rebuild
            astTimeStamp = 0;
            rebuildASTView();
 
            editor.getParser().addParseListener(getParserObserver());
        }
    }
 
    /**
     * Get an observer for parser events. If the field parserObserver is null,
     * it is filled with a new observer.
     * 
     * @return parserObserver, filled if it was null before
     */
    private IParserObserver getParserObserver() {
        if (parserObserver == null) {
            parserObserver = new IParserObserver() {
                @Override
                public void parserError(Throwable error, IAdaptable file, IDocument doc) {
                    Display.getDefault().asyncExec(new Runnable() {
                        public void run() {
                            clearASTView();
                        }
                    });
                }
 
                @Override
                public void parserChanged(ISimpleNode root, IAdaptable file, IDocument doc) {
                    Display.getDefault().asyncExec(new Runnable() {
                        public void run() {
                            rebuildASTView();
                        }
                    });
                }
            };
        }
        return parserObserver;
    }
 
    /**
     * Clear the tree.
     */
    private void clearASTView() {
        viewer.setInput(null);
    }
 
    /**
     * Rebuild the tree if necessary. Rebuilding is necessary if there is a
     * timestamp mismatch.
     */
    private void rebuildASTView() {
        long editorTS = editor.getAstModificationTimeStamp();
        if (astTimeStamp == 0 || astTimeStamp != editorTS) {
            viewer.setInput(editor.getAST());
            viewer.expandAll();
            astTimeStamp = editorTS;
        }
    }
 
    /**
     * Disconnect listeners.
     */
    @Override
    public void dispose() {
        if (editor != null && parserObserver != null)
            editor.getParser().removeParseListener(parserObserver);
        if (pageSelectionListener != null)
            getSite().getPage().removePostSelectionListener(
                    pageSelectionListener);
        super.dispose();
    }
 
    /**
     * Install editor file listener.
     */
    private void hookPageSelection() {
        pageSelectionListener = new ISelectionListener() {
            public void selectionChanged(IWorkbenchPart part,
                    ISelection selection) {
                pageSelectionChanged(part, selection);
            }
        };
        getSite().getPage().addPostSelectionListener(pageSelectionListener);
    }
 
    /**
     * Install listener for double clicks.
     */
    private void hookDoubleClickCommand() {
        viewer.addDoubleClickListener(new IDoubleClickListener() {
            public void doubleClick(DoubleClickEvent event) {
                if (editor == null)
                    return;
                ISelection s = viewer.getSelection();
                if (!(s instanceof IStructuredSelection))
                    return;
                Iterator<?> it = ((IStructuredSelection) s).iterator();
                if (!it.hasNext())
                    return;
                Object o = it.next();
                if (it.hasNext())
                    // multiple selection
                    return;
                SimpleNode sn = null;
                if (o instanceof NodeWrap) {
                    sn = ((NodeWrap) o).getASTNode();
                } else if (o instanceof FieldWrap) {
                    FieldWrap fw = ((FieldWrap) o);
                    NodeWrap nw = (NodeWrap) fw.getParent();
                    sn = nw.getASTNode();
                }
                if (sn == null)
                    return;
                try {
                    int offset = editor.getDocument().getLineOffset(sn.beginLine - 1) + sn.beginColumn - 1;
                    int length = 1; // select one character to make the
                                    // selection visible
                    if (sn instanceof Name)
                        length = ((Name) sn).id.length();
                    else if (sn instanceof NameTok)
                        length = ((NameTok) sn).id.length();
                    else if (sn instanceof Num)
                        length = ((Num) sn).num.length();
                    editor.setSelection(offset, length);
                } catch (BadLocationException e) {
                    // ignore
                }
            }
        });
    }
 
    /**
     * Focus the tree.
     */
    public void setFocus() {
        viewer.getControl().setFocus();
    }
 
}

PyASTWrap

Die Klasse PyASTWrap wird als Basisklasse für die Knoten des Baums im View verwendet.

package feu.k01919.q7649347.pyrefactor.astview;
 
/**
 * An abstract wrapper for AST-nodes for displaying in the PyDev AST Viewer.
 * @author Alexander Bürger <acfbuerger@googlemail.com>
 */
public abstract class PyASTWrap {
 
    /**
     * The parent wrapper node.
     */
    private PyASTWrap parent;
 
    /**
     * The name of the wrapped element.
     */
    private String name;
 
    /**
     * The wrapped children of the wrapped node.
     */
    private PyASTWrap[] children;
 
    /**
     * An empty array, used for nodes without children.
     */
    protected static final PyASTWrap[] EMPTY_CHILDLIST = { };
 
    /**
     * Initialize name and parent.
     * @param p  the parent wrapper node
     * @param n  the name of this wrapper
     */
    protected PyASTWrap(PyASTWrap p, String n) {
        parent = p;
        name = n;
    }
 
    /**
     * Retrieve the name of this wrapper.
     * This might be a field name, or a field name with an index.
     * @return the wrapper's name
     */
    public String getName() {
        return name;
    }
 
    /**
     * Retrieve the parent wrapper node.
     * @return the parent node
     */
    public PyASTWrap getParent() {
        return parent;
    }
 
    /**
     * Retrieve the list of children, building the list if necessary.
     * @return
     */
    public PyASTWrap[] getChildren() {
        if( children == null )
            children = makeChildList();
        return children;
    }
 
    /**
     * Get the value of the node like, e.g., the node type or the field contents.
     * @return the wrapper node's value
     */
    public abstract String getValue();
 
    /**
     * To be overwritten by children to build the list of children.
     * @return the list of children
     */
    protected abstract PyASTWrap[] makeChildList();
 
}

NodeWrap

In der Klasse NodeWrap werden die Knoten des AST als Knoten des Baums im AST-Viewer verpackt.

package feu.k01919.q7649347.pyrefactor.astview;
 
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
 
import org.python.pydev.parser.jython.SimpleNode;
 
/**
 * A wrapper for AST-nodes in order to display them in the
 * PyDev AST Viewer.
 * @author Alexander Bürger <acfbuerger@googlemail.com>
 */
public class NodeWrap extends PyASTWrap {
 
    /**
     * The wrapped AST node.
     */
    private SimpleNode node;
 
    /**
     * Initialize the wrapper for a single PyDev AST node.
     * @param p   the parent node
     * @param no  the AST node to wrap
     * @param n   the display name of this node
     */
    public NodeWrap(PyASTWrap p, SimpleNode no, String n) {
        super(p, n);
        node = no;
    }
 
    /**
     * Get the wrapped node.
     * @return the wrapped node
     */
    public SimpleNode getASTNode() {
        return node;
    }
 
    /**
     * Build a list of children.
     */
    @Override
    protected PyASTWrap[] makeChildList() {
        if( node == null )
            return EMPTY_CHILDLIST;
        List<PyASTWrap> c = new ArrayList<PyASTWrap>();
        Field[] fields = node.getClass().getDeclaredFields();
        for(Field f: fields) {
            try {
                if( SimpleNode[].class.isAssignableFrom(f.getType()) ) {
                    // child elements
                    c.add(new NodeSeqWrap(this, (SimpleNode[])f.get(node), f.getName()));
                } else if( SimpleNode.class.isAssignableFrom(f.getType()) ) {
                    // child element
                    c.add(new NodeWrap(this, (SimpleNode)f.get(node), f.getName()));
                } else {
                    c.add(new FieldWrap(this, node, f));
                }
            } catch( Exception e ) {
                System.out.println(" exception!");
                e.printStackTrace();
            }
        }
        return c.toArray(new PyASTWrap[c.size()]);
    }
 
    /**
     * Return the type of the node by inspecting the class name.
     * @return the class name (without package) or null
     */
    @Override
    public String getValue() {
        if( node == null )
            return "null";
        String cn = node.getClass().getName();
        return cn.substring(cn.lastIndexOf('.')+1);
    }
 
}

NodeSeqWrap

Die Klasse NodeSeqWrap verpackt eine Knotenliste des PyDev AST.

package feu.k01919.q7649347.pyrefactor.astview;
 
import java.util.ArrayList;
import java.util.List;
 
import org.python.pydev.parser.jython.SimpleNode;
 
/**
 * A wrapper for AST-node lists in order to display them in the
 * PyDev AST Viewer.
 * @author Alexander Bürger <acfbuerger@googlemail.com>
 */
public class NodeSeqWrap extends PyASTWrap {
 
    /**
     * The child AST nodes.
     */
    private SimpleNode[] nodes;
 
    /**
     * Initialize the wrapper for a AST node list.
     * @param p   the parent AST node wrapper
     * @param no  the node list to wrap
     * @param n   the name of this wrapper, i.e., the name of the field in the parent node
     */
    public NodeSeqWrap(PyASTWrap p, SimpleNode[] no, String n) {
        super(p, n);
        nodes = no;
    }
 
    /**
     * Create a list of children.
     * @return an array containing all children, or an empty list
     */
    @Override
    protected PyASTWrap[] makeChildList() {
        if( nodes == null )
            return EMPTY_CHILDLIST;
 
        List<PyASTWrap> c = new ArrayList<PyASTWrap>();
        for(int n=0; n<nodes.length; ++n)
            c.add(new NodeWrap(this, nodes[n], getName()+"["+n+"]"));
        return c.toArray(new PyASTWrap[c.size()]);
    }
 
    /**
     * The lists value.
     * @return a constant value indicating that this is a sequence
     */
    @Override
    public String getValue() {
        return "[...]";
    }
 
}

FieldWrap

Die Klasse FieldWrap verpackt ein Attribut einer Knotenklasse des PyDev AST als Knoten im Viewer-Baum.

package feu.k01919.q7649347.pyrefactor.astview;
 
import java.lang.reflect.Array;
import java.lang.reflect.Field;
 
import org.python.pydev.parser.jython.SimpleNode;
 
/**
 * A wrapper for AST-node fields in order to display them in the
 * PyDev AST Viewer.
 * @author Alexander Bürger <acfbuerger@googlemail.com>
 */
public class FieldWrap extends PyASTWrap {
 
    /**
     * The value will be calculated in the constructor and kept.
     */
    private String value;
 
    /**
     * Initialize the field wrapper, extracting the value.
     * @param p  the parent node's wrapper
     * @param node  the AST node to wrap
     * @param field  the field to display
     */
    public FieldWrap(NodeWrap p, SimpleNode node, Field field) {
        super(p, field.getName());
        value = makeStringFromField(field, node);
    }
 
    /**
     * A field has no children.
     * @return an empty array
     */
    @Override
    protected PyASTWrap[] makeChildList() {
        return EMPTY_CHILDLIST;
    }
 
    /**
     * Return the contents of the field.
     * @return the field's contents
     */
    @Override
    public String getValue() {
        return value;
    }
 
    /**
     * Convert field contents to a String.
     * @param field the field to read out
     * @param obj the instance containing the field
     * @return a String representing the field
     */
    public static String makeStringFromField(Field field, Object obj) {
        String value;
        try {
            if(field.getType().isArray()) {
                Object array = (Object) field.get(obj);
                int length = Array.getLength(array);
                value = "[";
                for(int i=0; i<length; ++i) {
                    value += Array.get(array, i);
                    if( i+1<length )
                        value += ",";
                }
                value += "]";
            } else {
                value = ""+field.get(obj);
            }
        } catch( IllegalAccessException e ) {
            value = "<access>";
        }
        return value;
    }
 
}

NameLabelProvider

Die Klasse NameLabelProvider wird verwendet, um die Spalte "Name" im AST Viewer darzustellen. Insbesondere werden hier die passenden Icons für die verschiedenen Knotentypen ausgewählt.

package feu.k01919.q7649347.pyrefactor.astview;
 
import org.eclipse.jface.viewers.ColumnLabelProvider;
import org.eclipse.swt.graphics.Image;
 
import feu.k01919.q7649347.pyrefactor.Activator;
 
/**
 * Provide labels and icons for the "name" column of the PyDev AST Viewer.
 * @author Alexander Bürger <acfbuerger@googlemail.com>
 */
public class NameLabelProvider extends ColumnLabelProvider {
 
    /**
     * The Image shown for nodes that are visited by VisitorIF implementations.
     */
    private Image imgNodeVisited;
 
    /**
     * The Image shown for nodes that are not visited by VisitorIF implementations.
     */
    private Image imgNodeNotVisited;
 
    /**
     * The Image shown for AST-node attributes.
     */
    private Image imgField;
 
    /**
     * The Image shown for AST-node sequences.
     */
    private Image imgNodeSeq;
 
    /**
     * The Image shown for null AST-nodes.
     */
    private Image imgNull;
 
    /**
     * Initialize by fetching all the images.
     */
    public NameLabelProvider() {
        imgNodeVisited = Activator.getImageDescriptor("icons/green-dot.png").createImage();
        imgNodeNotVisited = Activator.getImageDescriptor("icons/red-square.png").createImage();
        imgField = Activator.getImageDescriptor("icons/blue-dot.png").createImage();
        imgNodeSeq = Activator.getImageDescriptor("icons/gray-square.png").createImage();
        imgNull = Activator.getImageDescriptor("icons/gray-dot.png").createImage();
    }
 
    /**
     * Get the text for the "Name" column of the AST View.
     * @param obj the PyASTWrap object to be displayed
     * @return the result of PyASTWrap.getName()
     */
    public String getText(Object obj) {
        PyASTWrap p = (PyASTWrap) obj;
        return p.getName();
    }
 
    /**
     * Return an Image suitable for the given node wrapper.
     * A node will be declared as "visited by Visitor" depending on the type of
     * the wrapped AST node. This follows after inspection of the classes generated
     * by asdl_java.py in the plugin org.python.pydev.parser and package 
     * org.python.pydev.parser.jython.ast.
     * @param obj the PyASTWrap object to be displayed
     * @return one of the Image members of this class
     */
    public Image getImage(Object obj) {
        if( obj instanceof FieldWrap )
            return imgField;
        if( obj instanceof NodeSeqWrap )
            return imgNodeSeq;
        if( !(obj instanceof NodeWrap) )
            return null;
        NodeWrap nw = (NodeWrap)obj;
        if( nw.getASTNode() == null )
            return imgNull;
        boolean notVisited = ((NodeWrap)obj).getValue().endsWith("Type");
        return notVisited ? imgNodeNotVisited : imgNodeVisited;
    }
 
    /**
     * Release the Image objects.
     */
    public void dispose() {
        imgNodeVisited.dispose();
        imgNodeNotVisited.dispose();
        imgField.dispose();
        imgNodeSeq.dispose();
        imgNull.dispose();
        super.dispose();
    }
 
}

ValueLabelProvider

Mithilfe der Klasse ValueLabelProvider wird die Spalte "Value" im AST Viewer dargestellt.

package feu.k01919.q7649347.pyrefactor.astview;
 
import org.eclipse.jface.viewers.ColumnLabelProvider;
 
/**
 * Provide labels for the "Value" column of the PyDev AST Viewer.
 * @author Alexander Bürger <acfbuerger@googlemail.com>
 */
public class ValueLabelProvider extends ColumnLabelProvider {
 
    /**
     * Get the text for the "Value" column of the AST View.
     * @param obj the PyASTWrap object to be displayed
     * @return the result of PyASTWrap.getValue()
     */
    public String getText(Object obj) {
        PyASTWrap p = (PyASTWrap) obj;
        return p.getValue();
    }
}

ASTContentProvider

Die Klasse ASTContentProvider liefert im Wesentlichen die Wurzelknoten des Baums im AST Viewer.

package feu.k01919.q7649347.pyrefactor.astview;
 
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.Viewer;
import org.python.pydev.parser.jython.SimpleNode;
 
/**
 * A content provider for AST-node wrappers. This wrapper forwards almost all
 * calls to the PyASTWrap instamces.
 * 
 * @author Alexander Bürger <acfbuerger@googlemail.com>
 */
public class ASTContentProvider implements ITreeContentProvider {
 
    /**
     * The root element of the parse tree. This is usually an instance of
     * Module.
     */
    private PyASTWrap root;
 
    /**
     * Create a new root node if the parse tree changed.
     */
    public void inputChanged(Viewer v, Object oldInput, Object newInput) {
        root = new NodeWrap(null, (SimpleNode) newInput, "--root--");
    }
 
    /**
     * Nothing to do here.
     */
    public void dispose() {
    }
 
    /**
     * Return the root elements.
     * 
     * @return the root element.
     */
    public Object[] getElements(Object parent) {
        return getChildren(root);
        // return new PyASTWrap[] { root };
    }
 
    /**
     * Get the parent of a wrapper node.
     * 
     * @return the parent, may be null
     */
    public Object getParent(Object child) {
        return ((PyASTWrap) child).getParent();
    }
 
    /**
     * Get the children of a wrapper node.
     * 
     * @return the child list
     */
    public Object[] getChildren(Object parent) {
        return ((PyASTWrap) parent).getChildren();
    }
 
    /**
     * Check if a node has child nodes.
     * 
     * @return true if the node has children
     */
    public boolean hasChildren(Object parent) {
        return ((PyASTWrap) parent).getChildren().length > 0;
    }
 
}