blob: 5febc2284b926e6aa66c96f5962f0fdb8b4432fe [file] [log] [blame]
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();
}
}
}
}