org.checkerframework.common.util.TypeVisualizer Maven / Gradle / Ivy
Show all versions of checker Show documentation
package org.checkerframework.common.util;
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;
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;
/**
* 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).
*
* Currently, to use TypeVisualizer just insert an if statement somewhere that targets the type
* you would like to print: e.g.
*
*
{@code
* if (type.getKind() == TypeKind.EXECUTABLE && type.toString().contains("methodToPrint")) {
* TypeVisualizer.drawToPng("/Users/jburke/Documents/tmp/method.png", type);
* }
* }
*
* 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(File dest, AnnotatedTypeMirror type) {
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(String dest, 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(File dest, AnnotatedTypeMirror type) {
try {
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(String dest, 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(File dotFile, 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(
AnnotatedTypeVariable typeVariable, List typeVarNames, 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(
AnnotatedTypeVariable typeVariable, List typeVarNames, String directory) {
return printTypevarIfMatches(typeVariable, typeVarNames, directory, true);
}
private static boolean printTypevarIfMatches(
AnnotatedTypeVariable typeVariable,
List typeVarNames,
String directory,
boolean png) {
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.
*
* 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(@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 nodes = new LinkedHashMap<>();
/** List of connections between nodes. Lines refer to identifiers in nodes.values(). */
private final List 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(String graphName, AnnotatedTypeMirror type) {
this.graphName = graphName;
this.type = type;
}
public void draw(File file) {
addNodes(type);
addConnections();
write(file);
}
private void addNodes(AnnotatedTypeMirror type) {
new NodeDrawer().visit(type);
}
private void addConnections() {
ConnectionDrawer connectionDrawer = new ConnectionDrawer();
for (Node node : nodes.keySet()) {
connectionDrawer.visit(node.type);
}
}
/**
* Write this to a file.
*
* @param file the file to write to
*/
private void write(File file) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
writer.write("digraph " + graphName + "{");
writer.newLine();
for (String line : lines) {
writer.write(line + ";");
writer.newLine();
}
writer.write("}");
writer.flush();
} 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 {
@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) {
List 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) {
List 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) {
List 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"));
List typeVarElems = methodElem.getTypeParameters();
List typeVars = type.getTypeVariables();
for (int i = 0; i < typeVars.size(); i++) {
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"));
}
List paramElems = methodElem.getParameters();
List params = type.getParameterTypes();
for (int i = 0; i < params.size(); i++) {
String paramName = paramElems.get(i).getSimpleName().toString();
lines.add(connect(type, params.get(i)) + " " + makeParamLabel(paramName));
}
List 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(AnnotatedTypeMirror from, AnnotatedTypeMirror to) {
return nodes.get(new Node(from)) + " -> " + nodes.get(new Node(to));
}
private String makeLabel(String text) {
return "[label=\"" + text + "\"]";
}
private String makeTypeArgLabel(int argIndex) {
return makeLabel("<" + argIndex + ">");
}
private String makeMethodTypeArgLabel(String paramName) {
return makeLabel("<" + paramName + ">");
}
private String makeParamLabel(String paramName) {
return makeLabel(paramName);
}
private String makeThrownLabel(int index) {
return makeLabel("throws: " + index);
}
}
/** The default annotation formatter. */
private static final DefaultAnnotationFormatter annoFormatter =
new DefaultAnnotationFormatter();
/**
* Scans types and adds a mapping from type to dot node declaration representing that type
* in the enclosing drawing.
*/
private class NodeDrawer implements AnnotatedTypeVisitor {
/** Create a new NodeDrawer. */
public NodeDrawer() {}
private void visitAll(List types) {
for (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(AnnotatedTypeMirror atm) {
StringJoiner sj = new StringJoiner(" ");
for (AnnotationMirror anno : atm.getAnnotations()) {
// TODO: More comprehensive escaping
sj.add(annoFormatter.formatAnnotationMirror(anno).replace("\"", "\\"));
}
return sj.toString();
}
public boolean checkOrAdd(AnnotatedTypeMirror atm) {
Node node = new Node(atm);
if (nodes.containsKey(node)) {
return false;
}
nodes.put(node, String.valueOf(nextId++));
return true;
}
public String makeLabeledNode(AnnotatedTypeMirror type, String label) {
return makeLabeledNode(type, label, null);
}
public String makeLabeledNode(
AnnotatedTypeMirror type, String label, String attributes) {
String attr = (attributes != null) ? ", " + attributes : "";
return nodes.get(new Node(type)) + " [label=\"" + label + "\"" + attr + "]";
}
public void addLabeledNode(AnnotatedTypeMirror type, String label) {
lines.add(makeLabeledNode(type, label));
}
public void addLabeledNode(AnnotatedTypeMirror type, String label, String attributes) {
lines.add(makeLabeledNode(type, label, attributes));
}
public String makeMethodLabel(AnnotatedExecutableType methodType) {
ExecutableElement methodElem = methodType.getElement();
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();
}
}
}
}