package grafos; import com.github.javaparser.ast.Node; import com.github.javaparser.ast.expr.BooleanLiteralExpr; import com.github.javaparser.ast.expr.SimpleName; import com.github.javaparser.ast.stmt.*; import java.util.*; /** * Represents a control flow graph, offering methods to build it easily with a visitor and an output ready to be * converted to a graph by GraphViz ({@link #toStringList(GraphViz)}). *
* The process should begin with {@link #beginMethod()} and end with {@link #finishMethod()}. Similarly, every opened * {@link Block} and section must be closed. The class offers special support for {@link ContinueStmt} and {@link BreakStmt}, * including both the labeled and unlabeled versions. *
* The inner class {@link Block} is used to represent a block statement (not only {@link BlockStmt} but also {@link IfStmt} * and {@link WhileStmt}, for example). {@link Block} should not be used directly, only through the API provided by CFG. */ public class CFG { /** * List of nodes that exist in this CFG (can either be {@link com.github.javaparser.ast.expr.Expression} or {@link Statement}) */ private final List nodes = new LinkedList<>(); /** List of blocks (statements that contain other statements) in this CFG. * Only includes the "surface" blocks, not the ones inside other blocks. * @see Block */ private final LinkedList blocks = new LinkedList<>(); /** Stack of blocks, representing the position in the method. The current position is inside the topmost block, * which is contained inside the previous one. If empty, the position is outside all blocks, in the main level * of the method. */ private final Stack blockStack = new Stack<>(); /** List of {@link Edge}, or connections between nodes. */ private final List edges = new LinkedList<>(); /** First and last node of a method, represented by an {@link EmptyStmt} instance. */ private final Node beginNode = new EmptyStmt(), endNode = new EmptyStmt(); /** List of nodes that are immediately before the next instruction, and therefore will be linked to it. * May be empty after a break/return/continue, or contain more than one element after a control-flow instruction. */ private final LinkedList nodeList = new LinkedList<>(); /** Map of nodes to the name that needs to be applied to the edge when that node is connected to the next one. * Used to distinguish the {@code true} and {@code false} branches in conditions. */ private final Map labelMap = new HashMap<>(); /** Map of loop labels to lists of break statements (all labeled). Each list begins empty and {@link BreakStmt} are * added as found. When the end of the labeled loop is reached, the list is removed and dumped into {@link #nodeList}. */ private final Map> breakMap = new HashMap<>(); /** Stack of lists of break statements (without label). A new list is added when entering any structure that may be broken out of, * and the list is dumped into the {@link #nodeList} when exiting the loop. */ private final Stack> breakStack = new Stack<>(); /** Map of loop labels to the node that the loop "continues" to, i.e., the node to which a continue stmt will be * connected. */ private final Map continueMap = new HashMap<>(); /** Stack of the node that the loop "continues" to. * @see #continueMap */ private final Stack continueStack = new Stack<>(); /** Last labeled visited, if a {@link LabeledStmt} has been visited, but the corresponding loop hasn't been yet. * Otherwise, {@code null}. */ private SimpleName lastLabel; /** Adds a node to the list of nodes known by this tree */ private void registerNode(Node stmt) { nodes.add(stmt); if (!blockStack.isEmpty()) blockStack.peek().add(stmt); System.out.println("NODO: " + node2str(stmt)); } /** Clears the list of nodes and adds the given node. */ public void setNode(Node node) { clearNode(); appendNode(node); } /** Clears the list of nodes. */ public void clearNode() { nodeList.clear(); } /** Connect each node in the list of nodes to the current node, then set the node as the only element in the list. */ public void connectTo(Node node) { connectTo(node, true); } /** Connect each node in the list of nodes to the current node, then optionally set the node as the only * element in the list. */ public void connectTo(Node node, boolean overwrite) { if (!nodesContains(node)) registerNode(node); for (Node begin : nodeList) connect(begin, node); if (overwrite) setNode(node); } /** Checks if an object is in the node registration list. Implemented to use {@code ==} instead of * {@link List#contains(Object)}, which uses {@link Object#equals(Object)}. */ private boolean nodesContains(Object object) { for (Node n : nodes) if (n == object) return true; return false; } /** Removes a node from the list of nodes */ public void removeNode(Node node) { nodeList.remove(node); } /** Adds a node to the list of nodes */ public void appendNode(Node node) { nodeList.add(node); } /** Returns an unmodifiable copy of the list of nodes */ public List getNodeList() { return Collections.unmodifiableList(nodeList); } /** Replace the list of nodes with this list. The argument may be reused without affecting the list of nodes. */ public void setNodeList(List nodes) { nodeList.clear(); nodeList.addAll(nodes); } // Method operations /** Begin a method: clear the list of nodes and add the "Start" node */ public void beginMethod() { nodeList.clear(); nodeList.add(beginNode); } /** Finish a method: connect each element of the list of nodes to the end (unless they are the end already) */ public void finishMethod() { for (Node begin : nodeList) if (begin != endNode) connect(begin, endNode); } // Block operations /** Start a new block, with a node that it represents and its condition. */ public void beginBlock(Node node, Node condition) { Block b = new Block(node, condition); if (condition != null) registerNode(condition); if (blockStack.isEmpty()) blocks.addFirst(b); else blockStack.peek().addSubBlock(b); blockStack.push(b); } /** End a block */ public void endBlock() { blockStack.pop(); } /** Set the label to be used when the last element of the list of nodes is connected to the next node. Run this * AFTER adding the node with {@link #appendNode(Node)}. */ public void setNextLabel(boolean b) { labelMap.put(nodeList.getLast(), String.valueOf(b)); } /** Connects one node with another, checking the map of labels and clearing the appropriate entry */ private void connect(Node begin, Node end) { edges.add(new Edge(begin, end, labelMap.get(begin))); labelMap.remove(begin); } public Node getEndNode() { return endNode; } // Break operations /** Begin a new list of break statements associated with the given label */ public void beginBreakSection(SimpleName name) { breakMap.put(name, new LinkedList<>()); } /** Begin a new list of unlabeled break statements */ public void beginBreakSection() { breakStack.push(new LinkedList<>()); } /** Connect a break statement to the chain, then add it to the appropriate list, so that when the switch/loop ends * it can be connected to the proper node, which is currently unknown. */ public void connectBreak(BreakStmt stmt) { connectTo(stmt); if (stmt.getLabel().isPresent()) { breakMap.get(stmt.getLabel().get()).add(stmt); } else { breakStack.peek().add(stmt); } clearNode(); } /** Ends a list of labeled break statements, adding all the break statements collected to the list of nodes. */ public void endBreakSection(SimpleName name) { nodeList.addAll(breakMap.remove(name)); } /** Ends a list of unlabeled break statements, adding all the break statements collected to the list of nodes. * Restores the previous list (if it exists), so that nested loops/switch statements work properly */ public void endBreakSection() { nodeList.addAll(breakStack.pop()); } // Continue operations /** Begin a labeled continue section (loop). Inside the loop, {@link #beginContinueSection(Node)} must be called for * this method to work properly. */ public void beginContinueSection(SimpleName name) { assert lastLabel == null; lastLabel = name; } /** Begin a continue section (loop). Will register the given node as the to-go node when a continue statement is * found, and optionally register it with the proper label if {@link #beginContinueSection(SimpleName)} has been * called immediately before. */ public void beginContinueSection(Node node) { continueStack.push(node); if (lastLabel != null) { assert !continueMap.containsKey(lastLabel); continueMap.put(lastLabel, node); lastLabel = null; } } /** Connect each statement in the list of nodes to the continue statement, then connect the continue to the proper * statement, depending on whether it has a label or not. */ public void connectContinue(ContinueStmt stmt) { connectTo(stmt); if (stmt.getLabel().isPresent()) { connectTo(continueMap.get(stmt.getLabel().get())); } else { connectTo(continueStack.peek()); } clearNode(); } /** Ends a labeled continue section */ public void endContinueSection(SimpleName name) { continueMap.remove(name); } /** Ends an unlabeled continue section (loop). Works with nested loops */ public void endContinueSection() { continueStack.pop(); } /** Outputs the contents of this object as a list of strings, ready to be parsed by dot and turned into a graph. */ public List toStringList(GraphViz gv) { List res = new LinkedList<>(); res.add(gv.start_graph()); for (Block b : blocks) res.addAll(b.toStringList(gv)); for (Edge e : edges) { res.add(e.toString()); } res.add(gv.end_graph()); return res; } /** Converts a node to string, prepending an index and line number, and escaping any quotations. */ private String node2str(Node n) { if (n == beginNode) return "Start"; if (n == endNode) return "Stop"; return String.format("\"(%d, %d) %s\"", indexOfNode(n) + 1, n.getRange().get().begin.line, escape(n.toString())); } /** Finds the index of a node in the node registration list. Throws an exception if unsuccessful. * This implementation is necessary to use {@code ==} instead of {@link Object#equals(Object)}. */ private int indexOfNode(Node n) { for (int i = 0; i < nodes.size(); i++) if (nodes.get(i) == n) return i; throw new RuntimeException("Internal error, can't find node in list"); } /** Escapes a string. Currently only escapes quotation marks. */ private String escape(String s) { return s.replace("\"", "\\\""); } /** Id for each subgraph/block (incremented with each new block) */ private static int clusterId = 0; /** Representation of a statement containing {@link Node}, such as an if, loop or switch statement */ class Block extends LinkedList { /** * The node that is being represented by this block */ private final Node container; /** * The condition to enter the current block, or the condition of the if */ private final Node condition; /** The sub-blocks present in this Block */ private final List inners = new LinkedList<>(); Block(Node container, Node condition) { this.container = container; this.condition = condition; } /** Add a sub-block to this block */ void addSubBlock(Block block) { inners.add(block); } /** Get a representation ready to be used in dot (of the subgraph) */ List toStringList(GraphViz gv) { List res = new LinkedList<>(); res.add(gv.start_subgraph(clusterId++)); // start res.add("label = \"" + getTitle() + "\""); // title of the sub-graph // Add the condition node with a special shape, then reset the shape back to the default if (condition != null) { res.add("node [ shape = rectangle ]; " + node2str(condition)); res.add("node [ shape = ellipse ]"); } // Add all inner blocks for (Block b : inners) { res.addAll(b.toStringList(gv)); } // Add all nodes as part of the sub-graph for (Node n : this) { res.add(node2str(n)); } res.add(gv.end_subgraph()); // end return res; } /** Generate a title for the sub-graph based on the block instruction it represents. */ String getTitle() { // Variables to be filled for String#format(String, Object[]) String template; Object[] filler; if (container instanceof IfStmt) { template = "if (%s)"; filler = new Object[]{((IfStmt) container).getCondition()}; } else if (container instanceof WhileStmt) { template = "while (%s)"; filler = new Object[]{((WhileStmt) container).getCondition()}; } else if (container instanceof DoStmt) { template = "do-while (%s)"; filler = new Object[]{((DoStmt) container).getCondition()}; } else if (container instanceof ForStmt) { template = "for (%s; %s; %s)"; filler = new Object[]{ ((ForStmt) container).getInitialization(), ((ForStmt) container).getCompare().orElse(new BooleanLiteralExpr(true)), ((ForStmt) container).getUpdate()}; } else if (container instanceof ForeachStmt) { template = "for (%s : %s)"; filler = new Object[]{ ((ForeachStmt) container).getVariable(), ((ForeachStmt) container).getIterable()}; } else if (container instanceof BlockStmt) { template = ""; filler = new Object[0]; } else if (container instanceof SwitchStmt) { template = "switch (%s)"; filler = new Object[]{((SwitchStmt) container).getSelector()}; } else { throw new RuntimeException("Unimplemented!!!"); } return String.format(template, filler); } } /** Representation of a connection between two nodes, with an optional label */ private class Edge { private final Node origin, destination; private final String label; private Edge(Node origin, Node destination, String label) { this.origin = origin; this.destination = destination; this.label = label; } @Override public String toString() { String s = node2str(origin) + " -> " + node2str(destination); if (label != null) { s += " [ label = \"" + label + "\" ]"; } return s; } } }