From 9aad602df33ec9eb67452ef7f6d6e22065241a5b Mon Sep 17 00:00:00 2001 From: Carlos Galindo Date: Wed, 10 Apr 2019 01:20:24 +0200 Subject: [PATCH] Documentation and comments --- src/main/java/grafos/CFG.java | 120 +++++++++++++++++++++--- src/main/java/grafos/Transformador.java | 4 + src/main/java/grafos/Visitador.java | 115 ++++++++++++++++------- 3 files changed, 195 insertions(+), 44 deletions(-) diff --git a/src/main/java/grafos/CFG.java b/src/main/java/grafos/CFG.java index 4b39dd6..3a5b9a2 100644 --- a/src/main/java/grafos/CFG.java +++ b/src/main/java/grafos/CFG.java @@ -7,21 +7,58 @@ 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()) @@ -29,19 +66,24 @@ public class CFG { 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); @@ -51,6 +93,8 @@ public class CFG { 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) @@ -58,38 +102,45 @@ public class CFG { 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); } - public void appendNodes(Collection nodes) { - nodeList.addAll(nodes); - } - + /** 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) @@ -101,14 +152,18 @@ public class CFG { 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); @@ -118,14 +173,20 @@ public class CFG { 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()) { @@ -136,19 +197,29 @@ public class CFG { 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) { @@ -158,6 +229,8 @@ public class CFG { } } + /** 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()) { @@ -168,14 +241,17 @@ public class CFG { 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()); @@ -188,12 +264,15 @@ public class CFG { 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) @@ -201,14 +280,25 @@ public class CFG { 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 { - private final Node container, condition; + /** + * 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) { @@ -216,29 +306,36 @@ public class CFG { 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++)); - res.add("label = \"" + getTitle() + "\""); + 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()); + 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) { @@ -269,22 +366,23 @@ public class CFG { filler = new Object[]{((SwitchStmt) container).getSelector()}; } else { throw new RuntimeException("Unimplemented!!!"); - // TODO: remove } return String.format(template, filler); } } - class Edge { + /** Representation of a connection between two nodes, with an optional label */ + private class Edge { private final Node origin, destination; private final String label; - Edge(Node origin, Node destination, 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) { diff --git a/src/main/java/grafos/Transformador.java b/src/main/java/grafos/Transformador.java index 6afbf22..0e09b22 100644 --- a/src/main/java/grafos/Transformador.java +++ b/src/main/java/grafos/Transformador.java @@ -19,6 +19,9 @@ public class Transformador { analyzeFile(file); } + /** + * Runs {@link #analyzeFile(File)} for each Java file found in this directory, recursively. + */ public static void analyzeDir(File dir) throws FileNotFoundException { for (File f : dir.listFiles()) { if (f.isDirectory()) @@ -28,6 +31,7 @@ public class Transformador { } } + /** Analyzes and generates a CFG in a PDF from a Java file. */ public static void analyzeFile(File file) throws FileNotFoundException { System.out.println("BEGIN FILE " + file.getPath()); // Ruta del fichero con el programa que vamos a transformar diff --git a/src/main/java/grafos/Visitador.java b/src/main/java/grafos/Visitador.java index 5f8563e..73577a0 100755 --- a/src/main/java/grafos/Visitador.java +++ b/src/main/java/grafos/Visitador.java @@ -11,6 +11,10 @@ import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import java.util.LinkedList; import java.util.List; +/** + * Visits a compilation unit and extracts the control flow graph, registering all the data in the argument to all + * functions: {@link CFG}. This class contains no information, all data is saved in the control flow graph. + */ public class Visitador extends VoidVisitorAdapter { @Override public void visit(MethodDeclaration methodDeclaration, CFG graph) { @@ -19,6 +23,10 @@ public class Visitador extends VoidVisitorAdapter { graph.finishMethod(); } + /** + * Return statements are connected to the previous statement and to the end node of the graph. + * Schema: {@code -> return -> Stop} + */ @Override public void visit(ReturnStmt n, CFG arg) { arg.connectTo(n); @@ -26,24 +34,29 @@ public class Visitador extends VoidVisitorAdapter { arg.clearNode(); } + /** If statements are set up as a block, with a condition and supports any configuration, including missing else, + * empty then/else blocks. */ @Override public void visit(IfStmt n, CFG graph) { - // Connect to the condition + // prev --> cond Node ifStart = n.getCondition(); graph.beginBlock(n, ifStart); graph.connectTo(ifStart); - // Connect condition to then block + // cond --> then (if true) graph.setNextLabel(true); n.getThenStmt().accept(this, graph); - // Save end of then block + // then --> next (saved for later) List newPrev = new LinkedList<>(graph.getNodeList()); - // Connect condition to else block + // Else processing (either connect cond --> else --> next or cond --> next) graph.setNode(ifStart); graph.setNextLabel(false); if (n.getElseStmt().isPresent()) { + // cond --> else (if false) n.getElseStmt().get().accept(this, graph); + // else --> next newPrev.addAll(graph.getNodeList()); } else { + // cond --> next (if false) newPrev.add(ifStart); } // Set ends of then/else as the new list @@ -51,6 +64,7 @@ public class Visitador extends VoidVisitorAdapter { graph.endBlock(); } + /** Registers the label used for the loop statement in the break and continue sections of the CFG */ @Override public void visit(LabeledStmt n, CFG graph) { graph.beginBreakSection(n.getLabel()); @@ -60,130 +74,165 @@ public class Visitador extends VoidVisitorAdapter { graph.endBreakSection(n.getLabel()); } + /** While statements are set up as a block. They also generate an unlabeled break and continue section. + * The schema followed is {@code -> cond -> body -> cond ->} */ @Override public void visit(WhileStmt n, CFG graph) { Node whileStart = n.getCondition(); + // Begin block/sections graph.beginBlock(n, whileStart); graph.beginBreakSection(); graph.beginContinueSection(whileStart); + // prev --> cond graph.connectTo(whileStart); + // cond --> body (if true) graph.setNextLabel(true); n.getBody().accept(this, graph); + // body --> cond graph.connectTo(whileStart); + // cond --> next (if false) graph.setNextLabel(false); + // End block/sections graph.endContinueSection(); graph.endBreakSection(); graph.endBlock(); } + /** Do while statement, set up as block. Generates break and continue section. Schema: + * {@code -> body -> cond -> body -> cond ->} */ @Override public void visit(DoStmt n, CFG graph) { Node condition = n.getCondition(); + // Begin block/section graph.beginBlock(n, condition); graph.beginBreakSection(); graph.beginContinueSection(condition); + // prev --> body + // cond --> body (if true) graph.appendNode(condition); n.getBody().accept(this, graph); + // body --> cond graph.connectTo(condition); + // cond --> next (if false) graph.setNextLabel(false); + // End block/section graph.endContinueSection(); graph.endBreakSection(); graph.endBlock(); } + /** For statement, set up as a block. Generates break and continue section. If the condition is empty, it will be + * set as a {@code true} literal expression. Schema: {@code -> initializer(s) -> cond -> body -> update(s) -> cond -> } */ @Override public void visit(ForStmt n, CFG graph) { + // Begin block and break sections (continue section needs the comparison, see below) graph.beginBlock(n, n.getCompare().orElse(null)); graph.beginBreakSection(); - // Initialization expressions - if (n.getInitialization() != null) { - for (Expression e : n.getInitialization()) { - graph.connectTo(e); - } - } - // Comparison expression - // TODO: shortcut conditions (||, &&) + // prev --> initializer(s), chained in serial + for (Expression e : n.getInitialization()) + graph.connectTo(e); + // Set the comparison expression to "true" if the expression was empty, as a safeguard and to ease the treatment + // of cases where the loop is of the form "for (;;) ;", which would require special processing. assert n.getTokenRange().isPresent(); if (!n.getCompare().isPresent()) n.setCompare(new BooleanLiteralExpr(n.getTokenRange().get(), true)); + // Begin continue section (with the comparison on hand) graph.beginContinueSection(n.getCompare().get()); + // initializer(s) --> cond graph.connectTo(n.getCompare().get()); + // cond --> body (if true) graph.setNextLabel(true); - // Loop body n.getBody().accept(this, graph); - // Update expressions - if (n.getUpdate() != null) - for (Expression e : n.getUpdate()) - graph.connectTo(e); - // Connect to comparison expression - // Set comparison as last possible statement + // body --> update(s), chained in serial + for (Expression e : n.getUpdate()) + graph.connectTo(e); + // update(s) --> comp graph.connectTo(n.getCompare().get()); + // comp --> next (if false) graph.setNextLabel(false); + // End block/section (all of them) graph.endContinueSection(); graph.endBreakSection(); graph.endBlock(); } + /** Foreach statement, set up as a block. Generates break and continue sections. Schema: {@code -> cond -> body -> cond ->} */ @Override public void visit(ForeachStmt n, CFG graph) { + // Create a copy of the foreach statement to use as the condition node. The alternative would be to "emulate" the + // foreach statement with a for statement. ForeachStmt copy = new ForeachStmt(n.getTokenRange().orElse(null), n.getVariable(), n.getIterable(), new EmptyStmt()); + // Begin block/section graph.beginBlock(n, copy); graph.beginBreakSection(); graph.beginContinueSection(copy); + // prev --> cond graph.connectTo(copy); + // cond --> body (if true) graph.setNextLabel(true); n.getBody().accept(this, graph); + // body --> cond graph.connectTo(copy); + // cond --> next (if false) graph.setNextLabel(false); graph.endContinueSection(); graph.endBreakSection(); graph.endBlock(); } + /** Switch entry, considered part of the condition of the switch. */ @Override public void visit(SwitchEntryStmt n, CFG graph) { - // Case header + // Case header (prev -> case EXPR) Node caseNode = new SwitchEntryStmt(n.getTokenRange().orElse(null), n.getLabel().orElse(null), new NodeList<>()); graph.connectTo(caseNode); - // Case body + // Case body (case EXPR --> body) n.getStatements().accept(this, graph); + // body --> next } + /** Switch statement, set up as a block. Generates an unlabeled break section. The statement begins with an expression, + * which is compared to each of the cases, entering through the first one that matches. Each case may end in a break + * or return statement, or not. When it doesn't the control falls through the next cases. If the switch doesn't have + * a default entry, the */ @Override public void visit(SwitchStmt n, CFG graph) { // Link previous statement to the switch's selector Node selectorNode = n.getSelector(); + // Begin block and break section graph.beginBlock(n, selectorNode); graph.beginBreakSection(); + // prev --> expr graph.connectTo(selectorNode); - // Analyze switch's cases + // expr --> each case (fallthrough by default, so case --> case too) for (SwitchEntryStmt entry : n.getEntries()) { - entry.accept(this, graph); - graph.appendNode(selectorNode); + entry.accept(this, graph); // expr && prev case --> case --> next case + graph.appendNode(selectorNode); // expr --> next case } // The next statement will be linked to: - // 1. All break statements that broke from the switch - // 2. If the switch doesn't have a default statement, the switch's selector - // 3. If the last entry doesn't break, to the last statement - graph.removeNode(selectorNode); - if (n.getEntries().get(n.getEntries().size() - 1).getLabel().isPresent()) - graph.appendNode(selectorNode); + // 1. All break statements that broke from the switch (done with break section) + // 2. If the switch doesn't have a default statement, the switch's selector (already present) + // 3. If the last entry doesn't break, to the last statement (present already) + // If the last case is a default case, remove the selector node from the list of nodes (see 2) + if (!n.getEntries().get(n.getEntries().size() - 1).getLabel().isPresent()) + graph.removeNode(selectorNode); + // End block and break section graph.endBreakSection(); graph.endBlock(); } @Override public void visit(BreakStmt n, CFG graph) { - graph.connectBreak(n); + graph.connectBreak(n); // prev --> break --> end of loop } @Override public void visit(ContinueStmt n, CFG graph) { - graph.connectContinue(n); + graph.connectContinue(n); // prev --> continue --> loop condition } @Override public void visit(ExpressionStmt es, CFG graph) { - graph.connectTo(es); + graph.connectTo(es); // prev --> es --> next } }