| package org.checkerframework.common.util; |
| |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.FileWriter; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.StringJoiner; |
| import javax.lang.model.element.AnnotationMirror; |
| import javax.lang.model.element.ExecutableElement; |
| import javax.lang.model.element.TypeParameterElement; |
| import javax.lang.model.element.VariableElement; |
| import org.checkerframework.checker.interning.qual.FindDistinct; |
| import org.checkerframework.checker.interning.qual.InternedDistinct; |
| import org.checkerframework.checker.nullness.qual.Nullable; |
| import org.checkerframework.framework.type.AnnotatedTypeMirror; |
| import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; |
| import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; |
| import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; |
| import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedIntersectionType; |
| import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedNoType; |
| import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedNullType; |
| import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedPrimitiveType; |
| import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable; |
| import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedUnionType; |
| import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedWildcardType; |
| import org.checkerframework.framework.type.visitor.AnnotatedTypeVisitor; |
| import org.checkerframework.framework.util.DefaultAnnotationFormatter; |
| import org.checkerframework.framework.util.ExecUtil; |
| import org.checkerframework.javacutil.BugInCF; |
| import org.plumelib.util.StringsPlume; |
| |
| /** |
| * TypeVisualizer prints AnnotatedTypeMirrors as a directed graph where each node is a type and an |
| * arrow is a reference. Arrows are labeled with the relationship that reference represents (e.g. an |
| * arrow marked "extends" starts from a type variable or wildcard type and points to the upper bound |
| * of that type). |
| * |
| * <p>Currently, to use TypeVisualizer just insert an if statement somewhere that targets the type |
| * you would like to print: e.g. |
| * |
| * <pre>{@code |
| * if (type.getKind() == TypeKind.EXECUTABLE && type.toString().contains("methodToPrint")) { |
| * TypeVisualizer.drawToPng("/Users/jburke/Documents/tmp/method.png", type); |
| * } |
| * }</pre> |
| * |
| * Be sure to remove such statements before committing your changes. |
| */ |
| public class TypeVisualizer { |
| |
| /** |
| * Creates a dot file at dest that contains a digraph for the structure of {@code type}. |
| * |
| * @param dest the destination dot file |
| * @param type the type to be written |
| */ |
| public static void drawToDot(final File dest, final AnnotatedTypeMirror type) { |
| final Drawing drawer = new Drawing("Type", type); |
| drawer.draw(dest); |
| } |
| |
| /** |
| * Creates a dot file at dest that contains a digraph for the structure of {@code type}. |
| * |
| * @param dest the destination dot file, this string will be directly passed to new File(dest) |
| * @param type the type to be written |
| */ |
| public static void drawToDot(final String dest, final AnnotatedTypeMirror type) { |
| drawToDot(new File(dest), type); |
| } |
| |
| /** |
| * Draws a dot file for type in a temporary directory then calls the "dot" program to convert that |
| * file into a png at the location dest. This method will fail if a temp file can't be created. |
| * |
| * @param dest the destination png file |
| * @param type the type to be written |
| */ |
| public static void drawToPng(final File dest, final AnnotatedTypeMirror type) { |
| try { |
| final File dotFile = File.createTempFile(dest.getName(), ".dot"); |
| drawToDot(dotFile, type); |
| execDotToPng(dotFile, dest); |
| |
| } catch (Exception exc) { |
| throw new RuntimeException(exc); |
| } |
| } |
| |
| /** |
| * Draws a dot file for type in a temporary directory then calls the "dot" program to convert that |
| * file into a png at the location dest. This method will fail if a temp file can't be created. |
| * |
| * @param dest the destination png file, this string will be directly passed to new File(dest) |
| * @param type the type to be written |
| */ |
| public static void drawToPng(final String dest, final AnnotatedTypeMirror type) { |
| drawToPng(new File(dest), type); |
| } |
| |
| /** |
| * Converts the given dot file to a png file at the specified location. This method calls the |
| * program "dot" from Runtime.exec and will fail if "dot" is not on your class path. |
| * |
| * @param dotFile the dot file to convert |
| * @param pngFile the destination of the resultant png file |
| */ |
| public static void execDotToPng(final File dotFile, final File pngFile) { |
| String[] cmd = |
| new String[] {"dot", "-Tpng", dotFile.getAbsolutePath(), "-o", pngFile.getAbsolutePath()}; |
| System.out.println("Printing dotFile: " + dotFile + " to loc: " + pngFile); |
| System.out.flush(); |
| ExecUtil.execute(cmd, System.out, System.err); |
| } |
| |
| /** |
| * If the name of typeVariable matches one in the list of typeVarNames, then print typeVariable to |
| * a dot file at {@code directory/varName}. |
| * |
| * @return true if the type variable was printed, otherwise false |
| */ |
| public static boolean printTypevarToDotIfMatches( |
| final AnnotatedTypeVariable typeVariable, |
| final List<String> typeVarNames, |
| final String directory) { |
| return printTypevarIfMatches(typeVariable, typeVarNames, directory, false); |
| } |
| |
| /** |
| * If the name of typeVariable matches one in the list of typeVarNames, then print typeVariable to |
| * a png file at {@code directory/varName.png}. |
| * |
| * @return true if the type variable was printed, otherwise false |
| */ |
| public static boolean printTypevarToPngIfMatches( |
| final AnnotatedTypeVariable typeVariable, |
| final List<String> typeVarNames, |
| final String directory) { |
| return printTypevarIfMatches(typeVariable, typeVarNames, directory, true); |
| } |
| |
| private static boolean printTypevarIfMatches( |
| final AnnotatedTypeVariable typeVariable, |
| final List<String> typeVarNames, |
| final String directory, |
| final boolean png) { |
| final String dirPath = |
| directory.endsWith(File.separator) ? directory : directory + File.separator; |
| String varName = typeVariable.getUnderlyingType().asElement().toString(); |
| |
| if (typeVarNames.contains(varName)) { |
| if (png) { |
| TypeVisualizer.drawToPng(dirPath + varName + ".png", typeVariable); |
| } else { |
| TypeVisualizer.drawToDot(dirPath + varName + ".dot", typeVariable); |
| } |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * This class exists because there is no LinkedIdentityHashMap. |
| * |
| * <p>Node is just a wrapper around type mirror that replaces .equals with referential equality. |
| * This is done to preserve the order types were traversed so that printing will occur in a |
| * hierarchical order. However, since there is no LinkedIdentityHashMap, it was easiest to just |
| * create a wrapper that performed referential equality on types and use a LinkedHashMap. |
| */ |
| private static class Node { |
| /** The delegate; that is, the wrapped value. */ |
| private final @InternedDistinct AnnotatedTypeMirror type; |
| |
| /** |
| * Create a new Node that wraps the given type. |
| * |
| * @param type the type that the newly-constructed Node represents |
| */ |
| private Node(final @FindDistinct AnnotatedTypeMirror type) { |
| this.type = type; |
| } |
| |
| @Override |
| public int hashCode() { |
| return type.hashCode(); |
| } |
| |
| @Override |
| public boolean equals(@Nullable Object obj) { |
| if (obj == null) { |
| return false; |
| } |
| if (obj instanceof Node) { |
| return ((Node) obj).type == this.type; |
| } |
| |
| return false; |
| } |
| } |
| |
| /** |
| * Drawing visits a type and writes a dot file to the location specified. It contains data |
| * structures to hold the intermediate dot information before printing. |
| */ |
| private static class Drawing { |
| /** A map from Node (type) to a dot string declaring that node. */ |
| private final Map<Node, String> nodes = new LinkedHashMap<>(); |
| |
| /** List of connections between nodes. Lines refer to identifiers in nodes.values(). */ |
| private final List<String> lines = new ArrayList<>(); |
| |
| private final String graphName; |
| |
| /** The type being drawn. */ |
| private final AnnotatedTypeMirror type; |
| |
| /** Used to identify nodes uniquely. This field is monotonically increasing. */ |
| private int nextId = 0; |
| |
| public Drawing(final String graphName, final AnnotatedTypeMirror type) { |
| this.graphName = graphName; |
| this.type = type; |
| } |
| |
| public void draw(final File file) { |
| addNodes(type); |
| addConnections(); |
| write(file); |
| } |
| |
| private void addNodes(final AnnotatedTypeMirror type) { |
| new NodeDrawer().visit(type); |
| } |
| |
| private void addConnections() { |
| final ConnectionDrawer connectionDrawer = new ConnectionDrawer(); |
| for (final Node node : nodes.keySet()) { |
| connectionDrawer.visit(node.type); |
| } |
| } |
| |
| private void write(final File file) { |
| try { |
| BufferedWriter writer = null; |
| try { |
| writer = new BufferedWriter(new FileWriter(file)); |
| writer.write("digraph " + graphName + "{"); |
| writer.newLine(); |
| for (final String line : lines) { |
| writer.write(line + ";"); |
| writer.newLine(); |
| } |
| |
| writer.write("}"); |
| writer.flush(); |
| } catch (IOException e) { |
| throw new BugInCF(e, "Exception visualizing type:%nfile=%s%ntype=%s", file, type); |
| } finally { |
| if (writer != null) { |
| writer.close(); |
| } |
| } |
| } catch (IOException exc) { |
| throw new BugInCF(exc, "Exception visualizing type:%nfile=%s%ntype=%s", file, type); |
| } |
| } |
| |
| /** |
| * Connection drawer is used to add the connections between all the nodes created by the |
| * NodeDrawer. It is not a scanner and is called on every node in the nodes map. |
| */ |
| private class ConnectionDrawer implements AnnotatedTypeVisitor<Void, Void> { |
| |
| @Override |
| public Void visit(AnnotatedTypeMirror type) { |
| type.accept(this, null); |
| return null; |
| } |
| |
| @Override |
| public Void visit(AnnotatedTypeMirror type, Void aVoid) { |
| return visit(type); |
| } |
| |
| @Override |
| public Void visitDeclared(AnnotatedDeclaredType type, Void aVoid) { |
| final List<AnnotatedTypeMirror> typeArgs = type.getTypeArguments(); |
| for (int i = 0; i < typeArgs.size(); i++) { |
| lines.add(connect(type, typeArgs.get(i)) + " " + makeTypeArgLabel(i)); |
| } |
| return null; |
| } |
| |
| @Override |
| public Void visitIntersection(AnnotatedIntersectionType type, Void aVoid) { |
| final List<AnnotatedTypeMirror> bounds = type.getBounds(); |
| for (int i = 0; i < bounds.size(); i++) { |
| lines.add(connect(type, bounds.get(i)) + " " + makeLabel("&")); |
| } |
| return null; |
| } |
| |
| @Override |
| public Void visitUnion(AnnotatedUnionType type, Void aVoid) { |
| final List<AnnotatedDeclaredType> alternatives = type.getAlternatives(); |
| for (int i = 0; i < alternatives.size(); i++) { |
| lines.add(connect(type, alternatives.get(i)) + " " + makeLabel("|")); |
| } |
| return null; |
| } |
| |
| @Override |
| public Void visitExecutable(AnnotatedExecutableType type, Void aVoid) { |
| |
| ExecutableElement methodElem = type.getElement(); |
| lines.add(connect(type, type.getReturnType()) + " " + makeLabel("returns")); |
| |
| final List<? extends TypeParameterElement> typeVarElems = methodElem.getTypeParameters(); |
| final List<AnnotatedTypeVariable> typeVars = type.getTypeVariables(); |
| for (int i = 0; i < typeVars.size(); i++) { |
| final String typeVarName = typeVarElems.get(i).getSimpleName().toString(); |
| lines.add(connect(type, typeVars.get(i)) + " " + makeMethodTypeArgLabel(typeVarName)); |
| } |
| |
| if (type.getReceiverType() != null) { |
| lines.add(connect(type, type.getReceiverType()) + " " + makeLabel("receiver")); |
| } |
| |
| final List<? extends VariableElement> paramElems = methodElem.getParameters(); |
| final List<AnnotatedTypeMirror> params = type.getParameterTypes(); |
| for (int i = 0; i < params.size(); i++) { |
| final String paramName = paramElems.get(i).getSimpleName().toString(); |
| lines.add(connect(type, params.get(i)) + " " + makeParamLabel(paramName)); |
| } |
| |
| final List<AnnotatedTypeMirror> thrown = type.getThrownTypes(); |
| for (int i = 0; i < thrown.size(); i++) { |
| lines.add(connect(type, thrown.get(i)) + " " + makeThrownLabel(i)); |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public Void visitArray(AnnotatedArrayType type, Void aVoid) { |
| lines.add(connect(type, type.getComponentType())); |
| return null; |
| } |
| |
| @Override |
| public Void visitTypeVariable(AnnotatedTypeVariable type, Void aVoid) { |
| lines.add(connect(type, type.getUpperBound()) + " " + makeLabel("extends")); |
| lines.add(connect(type, type.getLowerBound()) + " " + makeLabel("super")); |
| return null; |
| } |
| |
| @Override |
| public Void visitPrimitive(AnnotatedPrimitiveType type, Void aVoid) { |
| return null; |
| } |
| |
| @Override |
| public Void visitNoType(AnnotatedNoType type, Void aVoid) { |
| return null; |
| } |
| |
| @Override |
| public Void visitNull(AnnotatedNullType type, Void aVoid) { |
| return null; |
| } |
| |
| @Override |
| public Void visitWildcard(AnnotatedWildcardType type, Void aVoid) { |
| lines.add(connect(type, type.getExtendsBound()) + " " + makeLabel("extends")); |
| lines.add(connect(type, type.getSuperBound()) + " " + makeLabel("super")); |
| return null; |
| } |
| |
| private String connect(final AnnotatedTypeMirror from, final AnnotatedTypeMirror to) { |
| return nodes.get(new Node(from)) + " -> " + nodes.get(new Node(to)); |
| } |
| |
| private String makeLabel(final String text) { |
| return "[label=\"" + text + "\"]"; |
| } |
| |
| private String makeTypeArgLabel(final int argIndex) { |
| return makeLabel("<" + argIndex + ">"); |
| } |
| |
| private String makeMethodTypeArgLabel(final String paramName) { |
| return makeLabel("<" + paramName + ">"); |
| } |
| |
| private String makeParamLabel(final String paramName) { |
| return makeLabel(paramName); |
| } |
| |
| private String makeThrownLabel(final int index) { |
| return makeLabel("throws: " + index); |
| } |
| } |
| |
| /** |
| * Scans types and adds a mapping from type to dot node declaration representing that type in |
| * the enclosing drawing. |
| */ |
| private class NodeDrawer implements AnnotatedTypeVisitor<Void, Void> { |
| private final DefaultAnnotationFormatter annoFormatter = new DefaultAnnotationFormatter(); |
| |
| public NodeDrawer() {} |
| |
| private void visitAll(final List<? extends AnnotatedTypeMirror> types) { |
| for (final AnnotatedTypeMirror type : types) { |
| visit(type); |
| } |
| } |
| |
| @Override |
| public Void visit(AnnotatedTypeMirror type) { |
| if (type != null) { |
| type.accept(this, null); |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public Void visit(AnnotatedTypeMirror type, Void aVoid) { |
| return visit(type); |
| } |
| |
| @Override |
| public Void visitDeclared(AnnotatedDeclaredType type, Void aVoid) { |
| if (checkOrAdd(type)) { |
| addLabeledNode( |
| type, |
| getAnnoStr(type) |
| + " " |
| + type.getUnderlyingType().asElement().getSimpleName() |
| + (type.getTypeArguments().isEmpty() ? "" : "<...>"), |
| "shape=box"); |
| visitAll(type.getTypeArguments()); |
| } |
| return null; |
| } |
| |
| @Override |
| public Void visitIntersection(AnnotatedIntersectionType type, Void aVoid) { |
| if (checkOrAdd(type)) { |
| addLabeledNode(type, getAnnoStr(type) + " Intersection", "shape=octagon"); |
| visitAll(type.getBounds()); |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public Void visitUnion(AnnotatedUnionType type, Void aVoid) { |
| if (checkOrAdd(type)) { |
| addLabeledNode(type, getAnnoStr(type) + " Union", "shape=doubleoctagon"); |
| visitAll(type.getAlternatives()); |
| } |
| return null; |
| } |
| |
| @Override |
| public Void visitExecutable(AnnotatedExecutableType type, Void aVoid) { |
| if (checkOrAdd(type)) { |
| addLabeledNode(type, makeMethodLabel(type), "shape=box,peripheries=2"); |
| |
| visit(type.getReturnType()); |
| visitAll(type.getTypeVariables()); |
| |
| visit(type.getReceiverType()); |
| visitAll(type.getParameterTypes()); |
| |
| visitAll(type.getThrownTypes()); |
| |
| } else { |
| throw new BugInCF("Executable types should never be recursive%ntype=%s", type); |
| } |
| return null; |
| } |
| |
| @Override |
| public Void visitArray(AnnotatedArrayType type, Void aVoid) { |
| if (checkOrAdd(type)) { |
| addLabeledNode(type, getAnnoStr(type) + "[]"); |
| visit(type.getComponentType()); |
| } |
| return null; |
| } |
| |
| @Override |
| public Void visitTypeVariable(AnnotatedTypeVariable type, Void aVoid) { |
| if (checkOrAdd(type)) { |
| addLabeledNode( |
| type, |
| getAnnoStr(type) + " " + type.getUnderlyingType().asElement().getSimpleName(), |
| "shape=invtrapezium"); |
| visit(type.getUpperBound()); |
| visit(type.getLowerBound()); |
| } |
| return null; |
| } |
| |
| @Override |
| public Void visitPrimitive(AnnotatedPrimitiveType type, Void aVoid) { |
| if (checkOrAdd(type)) { |
| addLabeledNode(type, getAnnoStr(type) + " " + type.getKind()); |
| } |
| return null; |
| } |
| |
| @Override |
| public Void visitNoType(AnnotatedNoType type, Void aVoid) { |
| if (checkOrAdd(type)) { |
| addLabeledNode(type, getAnnoStr(type) + " None"); |
| } |
| return null; |
| } |
| |
| @Override |
| public Void visitNull(AnnotatedNullType type, Void aVoid) { |
| if (checkOrAdd(type)) { |
| addLabeledNode(type, getAnnoStr(type) + " NullType"); |
| } |
| return null; |
| } |
| |
| @Override |
| public Void visitWildcard(AnnotatedWildcardType type, Void aVoid) { |
| if (checkOrAdd(type)) { |
| addLabeledNode(type, getAnnoStr(type) + "?", "shape=trapezium"); |
| visit(type.getExtendsBound()); |
| visit(type.getSuperBound()); |
| } |
| return null; |
| } |
| |
| /** |
| * Returns a string representation of the annotations on a type. |
| * |
| * @param atm an annotated type |
| * @return a string representation of the annotations on {@code atm} |
| */ |
| public String getAnnoStr(final AnnotatedTypeMirror atm) { |
| StringJoiner sj = new StringJoiner(" "); |
| for (final AnnotationMirror anno : atm.getAnnotations()) { |
| // TODO: More comprehensive escaping |
| sj.add(annoFormatter.formatAnnotationMirror(anno).replace("\"", "\\")); |
| } |
| return sj.toString(); |
| } |
| |
| public boolean checkOrAdd(final AnnotatedTypeMirror atm) { |
| final Node node = new Node(atm); |
| if (nodes.containsKey(node)) { |
| return false; |
| } |
| nodes.put(node, String.valueOf(nextId++)); |
| return true; |
| } |
| |
| public String makeLabeledNode(final AnnotatedTypeMirror type, final String label) { |
| return makeLabeledNode(type, label, null); |
| } |
| |
| public String makeLabeledNode( |
| final AnnotatedTypeMirror type, final String label, final String attributes) { |
| final String attr = (attributes != null) ? ", " + attributes : ""; |
| return nodes.get(new Node(type)) + " [label=\"" + label + "\"" + attr + "]"; |
| } |
| |
| public void addLabeledNode(final AnnotatedTypeMirror type, final String label) { |
| lines.add(makeLabeledNode(type, label)); |
| } |
| |
| public void addLabeledNode( |
| final AnnotatedTypeMirror type, final String label, final String attributes) { |
| lines.add(makeLabeledNode(type, label, attributes)); |
| } |
| |
| public String makeMethodLabel(final AnnotatedExecutableType methodType) { |
| final ExecutableElement methodElem = methodType.getElement(); |
| |
| final StringBuilder builder = new StringBuilder(); |
| builder.append(methodElem.getReturnType().toString()); |
| builder.append(" <"); |
| |
| builder.append(StringsPlume.join(", ", methodElem.getTypeParameters())); |
| builder.append("> "); |
| |
| builder.append(methodElem.getSimpleName().toString()); |
| |
| builder.append("("); |
| builder.append(StringsPlume.join(",", methodElem.getParameters())); |
| builder.append(")"); |
| return builder.toString(); |
| } |
| } |
| } |
| } |