com.google.javascript.jscomp.DotFormatter Maven / Gradle / Ivy
Show all versions of closure-compiler-unshaded Show documentation
/*
* Copyright 2007 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp;
import static java.util.Comparator.comparing;
import com.google.javascript.jscomp.ControlFlowGraph.Branch;
import com.google.javascript.jscomp.graph.DiGraph.DiGraphEdge;
import com.google.javascript.jscomp.graph.DiGraph.DiGraphNode;
import com.google.javascript.jscomp.graph.GraphvizGraph;
import com.google.javascript.jscomp.graph.GraphvizGraph.GraphvizEdge;
import com.google.javascript.jscomp.graph.GraphvizGraph.GraphvizNode;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.jstype.JSType;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import org.jspecify.nullness.Nullable;
/**
* DotFormatter prints out a dot file of the Abstract Syntax Tree. For a detailed description of the
* dot format and visualization tool refer to Graphviz.
*
* Typical usage of this class logfile.write(new DotFormatter().toDot(node));
*
*
*
This class is not thread safe and should not be used without proper external
* synchronization.
*/
public final class DotFormatter {
private static final String INDENT = " ";
private static final String ARROW = " -> ";
private static final String LINE = " -- ";
private static final int MAX_LABEL_NAME_LENGTH = 10;
// stores the current assignment of node to keys
private final LinkedHashMap assignments = new LinkedHashMap<>();
// key count in order to assign a unique key to each node
private int keyCount = 0;
// the builder used to generate the dot diagram
private final Appendable builder;
private final @Nullable ControlFlowGraph cfg;
private final boolean printAnnotations;
/** For Testing Only */
private DotFormatter() {
this.builder = new StringBuilder();
this.cfg = null;
this.printAnnotations = false;
}
private DotFormatter(
Node n, ControlFlowGraph cfg, Appendable builder, boolean printAnnotations)
throws IOException {
this.cfg = cfg;
this.builder = builder;
this.printAnnotations = printAnnotations;
formatPreamble();
traverseNodes(n);
formatConclusion();
}
/**
* Converts an AST to dot representation.
*
* @param n the root of the AST described in the dot formatted string
* @return the dot representation of the AST
*/
public static String toDot(Node n) throws IOException {
return toDot(n, null);
}
/**
* Converts an AST to dot representation.
*
* @param n the root of the AST described in the dot formatted string
* @param inCFG Control Flow Graph.
* @return the dot representation of the AST
*/
static String toDot(Node n, @Nullable ControlFlowGraph inCFG) throws IOException {
StringBuilder builder = new StringBuilder();
DotFormatter unused = new DotFormatter(n, inCFG, builder, false);
return builder.toString();
}
/**
* Outputs a string in DOT format that presents the graph.
*
* @param graph Input graph.
* @return A string in Dot format that presents the graph.
*/
public static String toDot(GraphvizGraph graph) {
StringBuilder builder = new StringBuilder();
builder.append(graph.isDirected() ? "digraph" : "graph");
builder.append(INDENT);
builder.append(graph.getName());
builder.append(" {\n");
builder.append(INDENT);
builder.append("node [color=lightblue2, style=filled];\n");
final String edgeSymbol = graph.isDirected() ? ARROW : LINE;
List nodes = graph.getGraphvizNodes();
String[] nodeNames = new String[nodes.size()];
for (int i = 0; i < nodeNames.length; i++) {
GraphvizNode gNode = nodes.get(i);
nodeNames[i] =
gNode.getId()
+ " [label=\""
+ gNode.getLabel()
+ "\" color=\""
+ gNode.getColor()
+ "\"]";
}
// We sort the nodes so we get a deterministic output every time regardless
// of the implementation of the graph data structure.
Arrays.sort(nodeNames);
for (String nodeName : nodeNames) {
builder.append(INDENT);
builder.append(nodeName);
builder.append(";\n");
}
// Sort the list of edges so the output is deterministic
List edges = graph.getGraphvizEdges();
edges.sort(comparing(GraphvizEdge::getNode1Id).thenComparing(GraphvizEdge::getNode2Id));
for (GraphvizEdge edge : edges) {
builder.append(INDENT);
builder.append(edge.getNode1Id());
builder.append(edgeSymbol);
builder.append(edge.getNode2Id());
builder
.append(" [label=\"")
.append(edge.getLabel())
.append("\" color=\"")
.append(edge.getColor())
.append("\"]");
builder.append(";\n");
}
builder.append("}\n");
return builder.toString();
}
/**
* Converts an AST to dot representation and appends it to the given buffer.
*
* @param n the root of the AST described in the dot formatted string
* @param inCFG Control Flow Graph.
* @param builder A place to dump the graph.
*/
static void appendDot(Node n, ControlFlowGraph inCFG, Appendable builder)
throws IOException {
DotFormatter unused = new DotFormatter(n, inCFG, builder, false);
}
/** Creates a DotFormatter purely for testing DotFormatter's internal methods. */
static DotFormatter newInstanceForTesting() {
return new DotFormatter();
}
private void traverseNodes(Node parent) throws IOException {
// key
int keyParent = key(parent);
// edges
for (Node child = parent.getFirstChild(); child != null; child = child.getNext()) {
int keyChild = key(child);
builder.append(INDENT);
builder.append(formatNodeName(keyParent));
builder.append(ARROW);
builder.append(formatNodeName(keyChild));
builder.append(" [weight=1];\n");
traverseNodes(child);
}
// Flow Edges
if (cfg != null && cfg.hasNode(parent)) {
List extends DiGraphEdge> outEdges = cfg.getOutEdges(parent);
String[] edgeList = new String[outEdges.size()];
for (int i = 0; i < edgeList.length; i++) {
DiGraphEdge edge = outEdges.get(i);
DiGraphNode succ = edge.getDestination();
String toNode = null;
if (succ == cfg.getImplicitReturn()) {
toNode = "RETURN";
} else {
int keySucc = key(succ.getValue());
toNode = formatNodeName(keySucc);
}
edgeList[i] =
formatNodeName(keyParent)
+ ARROW
+ toNode
+ " [label=\""
+ edge.getValue()
+ "\", "
+ "fontcolor=\"red\", "
+ "weight=0.01, color=\"red\"];\n";
}
Arrays.sort(edgeList);
for (String element : edgeList) {
builder.append(INDENT);
builder.append(element);
}
}
}
int key(Node n) throws IOException {
Integer key = assignments.get(n);
if (key == null) {
key = keyCount++;
assignments.put(n, key);
builder.append(INDENT);
builder.append(formatNodeName(key));
builder.append(" [label=\"");
builder.append(n.getToken().toString());
if (n.isName() || n.isStringLit() || n.isGetProp() || n.isImportStar() || n.isStringKey()) {
builder.append(getNodeLabel(n));
}
JSType type = n.getJSType();
if (type != null) {
builder.append(" : ");
builder.append(type.toString());
}
if (printAnnotations && cfg != null && cfg.hasNode(n)) {
Object annotation = cfg.getNode(n).getAnnotation();
if (annotation != null) {
builder.append("\\n");
builder.append(annotation.toString());
}
}
builder.append("\"");
if (n.getJSDocInfo() != null) {
builder.append(" color=\"green\"");
}
builder.append("];\n");
}
return key;
}
private static String getNodeLabel(Node n) {
String content = n.getString();
if (content.length() > MAX_LABEL_NAME_LENGTH) {
content = content.substring(0, MAX_LABEL_NAME_LENGTH);
}
if (!content.isEmpty()) {
content = "(" + content + ")";
}
return content;
}
private static String formatNodeName(Integer key) {
return "node" + key;
}
private void formatPreamble() throws IOException {
builder.append("digraph AST {\n");
builder.append(INDENT);
builder.append("node [color=lightblue2, style=filled];\n");
}
private void formatConclusion() throws IOException {
builder.append("}\n");
}
}