/**
* Copyright 2008 Anders Hessellund
*
* 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.
*
* $Id: ControlFlowGraph.java,v 1.1 2008/01/17 18:48:18 hessellund Exp $
*/
package org.ofbiz.plugin.analysis;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.BreakStatement;
import org.eclipse.jdt.core.dom.CatchClause;
import org.eclipse.jdt.core.dom.DoStatement;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.ForStatement;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.StructuralPropertyDescriptor;
import org.eclipse.jdt.core.dom.SwitchCase;
import org.eclipse.jdt.core.dom.SwitchStatement;
import org.eclipse.jdt.core.dom.TryStatement;
import org.eclipse.jdt.core.dom.WhileStatement;
// inspired by source code from a compiler course by Robby
class ControlFlowGraph {
/** the method which we build the control flow graph for */
final MethodDeclaration method;
/** maps a {@link Statement} to its predecessors */
final Map<Statement, Set<Statement>> predecessors;
/** maps a {@link Statement} to its successors */
final Map<Statement, Set<Statement>> successors;
/**
* Holds the start {@link Statement} of the CFG. If the method body is empty
* then, the start {@link Statement} is equal to the end {@link Statement}.
*/
final Statement start;
/**
* Holds the end {@link Statement} of the CFG. The end {@link Statement} is
* always the method block body.
*/
final Statement end;
ControlFlowGraph(MethodDeclaration method) {
this.method = method;
Visitor visitor = new Visitor();
try {
method.accept(visitor);
} catch (RuntimeException e) {
// TODO : unnecessary to check cfg-building?
e.printStackTrace();
}
predecessors = visitor.predecessors;
successors = visitor.successors;
start = visitor.init;
end = visitor.last;
}
@Override public String toString() {
StringBuilder sb = new StringBuilder();
TreeSet<Statement> sorted =
new TreeSet<Statement>(new Comparator<Statement>(){
public int compare(Statement o1, Statement o2) {
if (o1.getStartPosition()>o2.getStartPosition())
return 1;
return -1;
}});
sorted.addAll(successors.keySet());
for (Statement statement : sorted) {
sb.append("Statement: "+Util.getFirstLine(statement)+"\n");
for (Statement succ : successors.get(statement)) {
sb.append("\t"+(succ==end?"virtual last\n":Util.getFirstLine(succ)+"\n"));
}
}
return sb.toString();
}
private static class Visitor extends ASTVisitor {
//-- fields
/** statements in method */
private Set<Statement> statements = new HashSet<Statement>();
/** statement -> Set of successors statements */
Map<Statement, Set<Statement>> relations = new HashMap<Statement, Set<Statement>>();
private Statement init, last;
private Map<Statement, Set<Statement>>
predecessors = new HashMap<Statement, Set<Statement>>(),
successors = new HashMap<Statement, Set<Statement>>();
//-- visitor methods
@SuppressWarnings("unchecked")
@Override public boolean visit(MethodDeclaration node) {
List statements = node.getBody().statements();
last = node.getBody(); // use method's body as virtual last
if (statements.size() == 0) {
init = last;
return false; // empty body
}
init = (Statement) statements.get(0);
addEdge(last(statements), last);
return true;
}
@SuppressWarnings("unchecked")
@Override public boolean visit(Block node) {
List statements = node.statements();
int size = statements.size();
for (int i = 0; i < size - 1; i++) {
Statement statement = (Statement) statements.get(i);
// temporarily add edge for if-statement
// addEdge doesn't really add if s is a return statement
// and its successor is not equal to last
addEdge(statement, (Statement) statements.get(i + 1));
}
// could be done faster (this loop is different from the one above)
if(isInTryBlock()) {
for (int i = 0; i < size ; i++) {
Statement statement = (Statement) statements.get(i);
if (!(statement instanceof TryStatement)) {
handleTryCatchFinally(statement);
}
}
}
return true;
}
/** find sequential next in case the statement is in a try-catch-block */
@SuppressWarnings("unchecked")
private Statement findNextStatementInParentList(ASTNode node) {
assert node != null;
Statement next;
ASTNode parent = node.getParent();
assert parent != null;
StructuralPropertyDescriptor location = node.getLocationInParent();
assert location != null;
if (location.isChildProperty()) {
next = findNextStatementInParentList(parent);
} else {
assert location.isChildListProperty();
List l = (List)parent.getStructuralProperty(location);
int index = l.indexOf(node);
if (index+1<l.size()) {
if (l.get(index+1) instanceof Statement) {
next = (Statement) l.get(index+1);
} else {
assert l.get(index+1) instanceof MethodDeclaration;
next = last;
}
} else {
next = findNextStatementInParentList(parent.getParent());
}
}
return next;
}
@SuppressWarnings("unchecked")
@Override public boolean visit(IfStatement node) {
Set<Statement> set = getStatements(relations, node);
Statement next;
if (set.size() > 1) {
next = findNextStatementInParentList(node);
} else {
assert set.size() == 1;
next = set.iterator().next();
}
assert next!=null;
relations.remove(node); // remove temporary edge for if-statement
// THEN
if (!(node.getThenStatement() instanceof Block)) {
addEdge(node,node.getThenStatement());
addEdge(node.getThenStatement(),next);
} else {
List thenList = ((Block) node.getThenStatement()).statements();
if (thenList.isEmpty()) {
addEdge(node, next);
} else {
addEdge(node, first(thenList));
addEdge(last(thenList), next);
}
}
// ELSE
if(node.getElseStatement()==null){
addEdge(node,next);
return true;
}
if (!(node.getElseStatement() instanceof Block)) {
addEdge(node,node.getElseStatement());
addEdge(node.getElseStatement(),next);
} else {
List elseList = ((Block) node.getElseStatement()).statements();
if (elseList.isEmpty()) {
addEdge(node, next);
} else {
addEdge(node, first(elseList));
addEdge(last(elseList), next);
}
}
return true;
}
@Override public boolean visit(WhileStatement node) {
if (!(node.getBody() instanceof Block)) {
addEdge(node,node.getBody());
addEdge(node.getBody(),node);
return true;
}
List statements = ((Block) node.getBody()).statements();
if (statements.isEmpty()) {
addEdge(node, node);
} else {
addEdge(node, first(statements));
addEdge(last(statements), node);
}
return true;
}
@Override public boolean visit(ReturnStatement node) {
addEdge(node, last);
return false;
}
@SuppressWarnings("unchecked")
@Override public boolean visit(DoStatement node) {
if (!(node.getBody() instanceof Block)) {
addEdge(node,node.getBody());
addEdge(node.getBody(),node);
return true;
}
List statements = ((Block) node.getBody()).statements();
if (statements.isEmpty()) {
addEdge(node, node);
} else {
addEdge(node, first(statements));
addEdge(last(statements), node);
}
return true;
}
@Override public boolean visit(EnhancedForStatement node) {
if (!(node.getBody() instanceof Block)) {
addEdge(node,node.getBody());
addEdge(node.getBody(),node);
return true;
}
List statements = ((Block) node.getBody()).statements();
if (statements.isEmpty()) {
addEdge(node, node);
} else {
addEdge(node, first(statements));
addEdge(last(statements), node);
}
return true;
}
@SuppressWarnings("unchecked")
@Override public boolean visit(ForStatement node) {
if (!(node.getBody() instanceof Block)) {
addEdge(node,node.getBody());
addEdge(node.getBody(),node);
return true;
}
List statements = ((Block) node.getBody()).statements();
if (statements.isEmpty()) {
addEdge(node, node);
} else {
addEdge(node, first(statements));
addEdge(last(statements), node);
}
return true;
}
/** TODO: handle <i>continue</i>*/
@Override public boolean visit(SwitchStatement node) {
Set<Statement> set = getStatements(relations, node);
assert set.size() == 1;
Statement next = set.iterator().next();
// switch stmt can not be skipped if there is a default branch
for(Object stmt : node.statements()) {
if (stmt instanceof SwitchCase) {
if (((SwitchCase)stmt).getExpression()==null) {
relations.remove(node);
}
}
}
Statement previous = null;
for(Object obj : node.statements()) {
Statement stmt = (Statement) obj;
if (previous!=null) {
addEdge(previous,stmt);
}
if (stmt instanceof SwitchCase) {
addEdge(node,stmt);
}
if (stmt instanceof BreakStatement) {
addEdge(stmt,next);
previous = null;
} else {
previous = stmt;
}
}
addEdge(last(node.statements()),next);
return true;
}
private List<TryStatement> tryStack = new ArrayList<TryStatement>();
private Map<Statement,Statement> try2Successor = new HashMap<Statement, Statement>();
@Override public boolean visit(TryStatement node) {
// push on stack
tryStack.add(node);
// add edges to try-body, remove temporary node similar to IFs
if (!node.getBody().statements().isEmpty()) {
Set<Statement> set = getStatements(relations, node);
assert set.size() == 1;
Statement next = set.iterator().next();
// remember successors
try2Successor.put(node, next);
relations.remove(node);
addEdge(node, first(node.getBody().statements()));
// finally-block requires an extra indirection
if (node.getFinally()!=null && !node.getFinally().statements().isEmpty()) {
addEdge(last(node.getBody().statements()),first(node.getFinally().statements()));
addEdge(last(node.getFinally().statements()),next);
} else {
addEdge(last(node.getBody().statements()),next);
}
}
return true;
}
@Override public boolean visit(CatchClause node) {
// pop from stack when the first catch/finally is met
if (isInTryBlock()) {
TryStatement tryStmt = tryStack.get(tryStack.size()-1);
if (tryStmt.catchClauses().get(0).equals(node)) {
tryStack.remove(tryStmt);
}
}
return super.visit(node);
}
private void handleTryCatchFinally(Statement stmt) {
// does this statement throw any checked exceptions?
final Set<ITypeBinding> exceptions = new HashSet<ITypeBinding>();
stmt.accept(new ASTVisitor(){
@Override public boolean visit(MethodInvocation node) {
IMethodBinding binding = node.resolveMethodBinding();
for(ITypeBinding itb : binding.getExceptionTypes()) {
if (isCheckedException(itb)) {
exceptions.add(itb);
}
}
return true;
}
});
// if this statement does not throw any exceptions,
// no extra edges are necessary
if (exceptions.size()==0) return;
// move from top to bottom of stack and remove exceptions
// as they are caught by catch clauses. Add edges on the way
// while observing a ny intermediate finally-clauses
int index = tryStack.size()-1;
//List<Block> finallyBlocks = new ArrayList<Block>();
while(index>=0) {
TryStatement tryStmt = tryStack.get(index);
for(int j = 0; j<tryStmt.catchClauses().size(); j++) {
CatchClause clause = (CatchClause) tryStmt.catchClauses().get(j);
SingleVariableDeclaration svd = clause.getException();
ITypeBinding caught = svd.getType().resolveBinding();
// ignore unchecked exceptions
if (!isCheckedException(caught)) continue;
Set<ITypeBinding> caughtExceptions = new HashSet<ITypeBinding>();
// iterate through thrown exceptions and
// check if they catch this exception
for(ITypeBinding thrown : exceptions) {
if (thrown.isSubTypeCompatible(caught)) {
// note this exception, so it can be filtered out on next iteration
caughtExceptions.add(thrown);
// what succeeds this try-stmt
Statement next = try2Successor.get(tryStmt);
// add extra edges for intermediate finally-blocks
Statement beginning = stmt;
for(int k = tryStack.size()-1; k>index; k--) {
TryStatement intermediate = tryStack.get(k);
if (intermediate.getFinally()!=null && !intermediate.getFinally().statements().isEmpty()) {
addEdge(beginning,first(intermediate.getFinally().statements()));
beginning = last(intermediate.getFinally().statements());
}
}
// add edge into catch clause
if (!clause.getBody().statements().isEmpty()) {
addEdge(beginning,first(clause.getBody().statements()));
// add edge into finally clause
if (tryStmt.getFinally()!= null && !tryStmt.getFinally().statements().isEmpty()) {
// case 1: non-empty catch and non-empty finally
addEdge(last(clause.getBody().statements()),first(tryStmt.getFinally().statements()));
addEdge(last(tryStmt.getFinally().statements()),next);
} else {
// case 2: non-empty catch and empty finally
addEdge(last(clause.getBody().statements()),next);
}
} else {
// add edge into finally clause
if (tryStmt.getFinally()!= null && !tryStmt.getFinally().statements().isEmpty()) {
// case 3: empty catch and non-empty finally
addEdge(beginning,first(tryStmt.getFinally().statements()));
addEdge(last(tryStmt.getFinally().statements()),next);
} else {
// case 4: empty catch and empty finally
addEdge(beginning,next);
}
}
}
}
// remove exceptions as they are caught
// cannot be done inside loop
exceptions.removeAll(caughtExceptions);
}
index--;
}
}
private boolean isInTryBlock() { return tryStack.size()>0; }
private boolean isCheckedException(ITypeBinding binding) {
while(binding != null) {
if (binding.getQualifiedName().equals("java.lang.RuntimeException")) {
return false;
}
if (binding.getQualifiedName().equals("java.lang.Exception")) {
return true;
}
binding = binding.getSuperclass();
}
return false;
}
//-- end visit
@Override public void endVisit(MethodDeclaration node) {
Set<Statement> reachableSet = new HashSet<Statement>();
computeSuccsPreds(reachableSet, init);
}
//-- utility methods
/** recursively initialize the predecessor and successor fields */
@SuppressWarnings("unchecked")
private void computeSuccsPreds(Set<Statement> set, Statement statement) {
if (set.contains(statement)) { return; }
set.add(statement);
for (Statement successor : getStatements(relations, statement)) {
getStatements(successors, statement).add(successor);
getStatements(predecessors, successor).add(statement);
computeSuccsPreds(set, successor);
}
}
private Statement first(List statements) {
assert !statements.isEmpty();
return (Statement) statements.get(0);
}
private Statement last(List statements) {
assert !statements.isEmpty();
return (Statement) statements.get(statements.size() - 1);
}
/** get the set corresponding to the statement
* and if no set exists return a new empty set */
private Set<Statement> getStatements(
Map<Statement, Set<Statement>> map, Statement statement) {
Set<Statement> set = map.get(statement);
if (set == null) {
set = new HashSet<Statement>();
map.put(statement, set);
}
return set;
}
private void addEdge(Statement s1, Statement s2) {
if (s1 instanceof ReturnStatement && s2 != last) {
return;
}
statements.add(s1);
statements.add(s2);
getStatements(relations, s1).add(s2);
}
}
}