blob: 3bda0b80c9102cd7bf7886a572c254cd34f22ef0 [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* 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.android.tools.lint.checks;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FrameNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.IntInsnNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TryCatchBlockNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.BasicInterpreter;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
//import org.objectweb.asm.util.Printer;
/**
* A {@linkplain ControlFlowGraph} is a graph containing a node for each
* instruction in a method, and an edge for each possible control flow; usually
* just "next" for the instruction following the current instruction, but in the
* case of a branch such as an "if", multiple edges to each successive location,
* or with a "goto", a single edge to the jumped-to instruction.
* <p>
* It also adds edges for abnormal control flow, such as the possibility of a
* method call throwing a runtime exception.
*/
public class ControlFlowGraph {
/** Map from instructions to nodes */
private Map<AbstractInsnNode, Node> mNodeMap;
private MethodNode mMethod;
/**
* Creates a new {@link ControlFlowGraph} and populates it with the flow
* control for the given method. If the optional {@code initial} parameter is
* provided with an existing graph, then the graph is simply populated, not
* created. This allows subclassing of the graph instance, if necessary.
*
* @param initial usually null, but can point to an existing instance of a
* {@link ControlFlowGraph} in which that graph is reused (but
* populated with new edges)
* @param classNode the class containing the method to be analyzed
* @param method the method to be analyzed
* @return a {@link ControlFlowGraph} with nodes for the control flow in the
* given method
* @throws AnalyzerException if the underlying bytecode library is unable to
* analyze the method bytecode
*/
@NonNull
public static ControlFlowGraph create(
@Nullable ControlFlowGraph initial,
@NonNull ClassNode classNode,
@NonNull MethodNode method) throws AnalyzerException {
final ControlFlowGraph graph = initial != null ? initial : new ControlFlowGraph();
final InsnList instructions = method.instructions;
graph.mNodeMap = Maps.newHashMapWithExpectedSize(instructions.size());
graph.mMethod = method;
// Create a flow control graph using ASM5's analyzer. According to the ASM 4 guide
// (download.forge.objectweb.org/asm/asm4-guide.pdf) there are faster ways to construct
// it, but those require a lot more code.
Analyzer analyzer = new Analyzer(new BasicInterpreter()) {
@Override
protected void newControlFlowEdge(int insn, int successor) {
// Update the information as of whether the this object has been
// initialized at the given instruction.
AbstractInsnNode from = instructions.get(insn);
AbstractInsnNode to = instructions.get(successor);
graph.add(from, to);
}
@Override
protected boolean newControlFlowExceptionEdge(int insn, TryCatchBlockNode tcb) {
AbstractInsnNode from = instructions.get(insn);
graph.exception(from, tcb);
return super.newControlFlowExceptionEdge(insn, tcb);
}
@Override
protected boolean newControlFlowExceptionEdge(int insn, int successor) {
AbstractInsnNode from = instructions.get(insn);
AbstractInsnNode to = instructions.get(successor);
graph.exception(from, to);
return super.newControlFlowExceptionEdge(insn, successor);
}
};
analyzer.analyze(classNode.name, method);
return graph;
}
/**
* Checks whether there is a path from the given source node to the given
* destination node
*/
@SuppressWarnings("MethodMayBeStatic")
private boolean isConnected(@NonNull Node from,
@NonNull Node to, @NonNull Set<Node> seen) {
if (from == to) {
return true;
} else if (seen.contains(from)) {
return false;
}
seen.add(from);
List<Node> successors = from.successors;
List<Node> exceptions = from.exceptions;
if (exceptions != null) {
for (Node successor : exceptions) {
if (isConnected(successor, to, seen)) {
return true;
}
}
}
if (successors != null) {
for (Node successor : successors) {
if (isConnected(successor, to, seen)) {
return true;
}
}
}
return false;
}
/**
* Checks whether there is a path from the given source node to the given
* destination node
*/
public boolean isConnected(@NonNull Node from, @NonNull Node to) {
return isConnected(from, to, Sets.<Node>newIdentityHashSet());
}
/**
* Checks whether there is a path from the given instruction to the given
* instruction node
*/
public boolean isConnected(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) {
return isConnected(getNode(from), getNode(to));
}
/** A {@link Node} is a node in the control flow graph for a method, pointing to
* the instruction and its possible successors */
public static class Node {
/** The instruction */
public final AbstractInsnNode instruction;
/** Any normal successors (e.g. following instruction, or goto or conditional flow) */
public final List<Node> successors = new ArrayList<Node>(2);
/** Any abnormal successors (e.g. the handler to go to following an exception) */
public final List<Node> exceptions = new ArrayList<Node>(1);
/** A tag for use during depth-first-search iteration of the graph etc */
public int visit;
/**
* Constructs a new control graph node
*
* @param instruction the instruction to associate with this node
*/
public Node(@NonNull AbstractInsnNode instruction) {
this.instruction = instruction;
}
void addSuccessor(@NonNull Node node) {
if (!successors.contains(node)) {
successors.add(node);
}
}
void addExceptionPath(@NonNull Node node) {
if (!exceptions.contains(node)) {
exceptions.add(node);
}
}
/**
* Represents this instruction as a string, for debugging purposes
*
* @param includeAdjacent whether it should include a display of
* adjacent nodes as well
* @return a string representation
*/
@NonNull
public String toString(boolean includeAdjacent) {
StringBuilder sb = new StringBuilder(100);
sb.append(getId(instruction));
sb.append(':');
if (instruction instanceof LabelNode) {
//LabelNode l = (LabelNode) instruction;
//sb.append('L' + l.getLabel().getOffset() + ":");
//sb.append('L' + l.getLabel().info + ":");
sb.append("LABEL");
} else if (instruction instanceof LineNumberNode) {
sb.append("LINENUMBER ").append(((LineNumberNode)instruction).line);
} else if (instruction instanceof FrameNode) {
sb.append("FRAME");
} else {
int opcode = instruction.getOpcode();
String opcodeName = getOpcodeName(opcode);
sb.append(opcodeName);
if (instruction.getType() == AbstractInsnNode.METHOD_INSN) {
sb.append('(').append(((MethodInsnNode)instruction).name).append(')');
}
}
if (includeAdjacent) {
if (successors != null && !successors.isEmpty()) {
sb.append(" Next:");
for (Node successor : successors) {
sb.append(' ');
sb.append(successor.toString(false));
}
}
if (exceptions != null && !exceptions.isEmpty()) {
sb.append(" Exceptions:");
for (Node exception : exceptions) {
sb.append(' ');
sb.append(exception.toString(false));
}
}
sb.append('\n');
}
return sb.toString();
}
}
/** Adds an exception flow to this graph */
protected void add(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) {
getNode(from).addSuccessor(getNode(to));
}
/** Adds an exception flow to this graph */
protected void exception(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) {
// For now, these edges appear useless; we also get more specific
// information via the TryCatchBlockNode which we use instead.
//getNode(from).addExceptionPath(getNode(to));
}
/** Adds an exception try block node to this graph */
protected void exception(@NonNull AbstractInsnNode from, @NonNull TryCatchBlockNode tcb) {
// Add tcb's to all instructions in the range
LabelNode start = tcb.start;
LabelNode end = tcb.end; // exclusive
// Add exception edges for all method calls in the range
AbstractInsnNode curr = start;
Node handlerNode = getNode(tcb.handler);
while (curr != end && curr != null) {
// A method can throw can exception, or a throw instruction directly
if (curr.getType() == AbstractInsnNode.METHOD_INSN
|| (curr.getType() == AbstractInsnNode.INSN
&& curr.getOpcode() == Opcodes.ATHROW)) {
// Method call; add exception edge to handler
if (tcb.type == null) {
// finally block: not an exception path
getNode(curr).addSuccessor(handlerNode);
}
getNode(curr).addExceptionPath(handlerNode);
}
curr = curr.getNext();
}
}
/**
* Looks up (and if necessary) creates a graph node for the given instruction
*
* @param instruction the instruction
* @return the control flow graph node corresponding to the given
* instruction
*/
@NonNull
public Node getNode(@NonNull AbstractInsnNode instruction) {
Node node = mNodeMap.get(instruction);
if (node == null) {
node = new Node(instruction);
mNodeMap.put(instruction, node);
}
return node;
}
/**
* Creates a human readable version of the graph
*
* @param start the starting instruction, or null if not known or to use the
* first instruction
* @return a string version of the graph
*/
@NonNull
public String toString(@Nullable Node start) {
StringBuilder sb = new StringBuilder(400);
AbstractInsnNode curr;
if (start != null) {
curr = start.instruction;
} else {
if (mNodeMap.isEmpty()) {
return "<empty>";
} else {
curr = mNodeMap.keySet().iterator().next();
while (curr.getPrevious() != null) {
curr = curr.getPrevious();
}
}
}
while (curr != null) {
Node node = mNodeMap.get(curr);
if (node != null) {
sb.append(node.toString(true));
}
curr = curr.getNext();
}
return sb.toString();
}
@Override
public String toString() {
return toString(null);
}
// ---- For debugging only ----
private static Map<Object, String> sIds = null;
private static int sNextId = 1;
private static String getId(Object object) {
if (sIds == null) {
sIds = Maps.newHashMap();
}
String id = sIds.get(object);
if (id == null) {
id = Integer.toString(sNextId++);
sIds.put(object, id);
}
return id;
}
/**
* Generates dot output of the graph. This can be used with
* graphwiz to visualize the graph. For example, if you
* save the output as graph1.gv you can run
* <pre>
* $ dot -Tps graph1.gv -o graph1.ps
* </pre>
* to generate a postscript file, which you can then view
* with "gv graph1.ps".
*
* (There are also some online web sites where you can
* paste in dot graphs and see the visualization right
* there in the browser.)
*
* @return a dot description of this control flow graph,
* useful for debugging
*/
@SuppressWarnings("unused")
public String toDot(@Nullable Set<Node> highlight) {
StringBuilder sb = new StringBuilder();
sb.append("digraph G {\n");
AbstractInsnNode instruction = mMethod.instructions.getFirst();
// Special start node
sb.append(" start -> ").append(getId(mNodeMap.get(instruction))).append(";\n");
sb.append(" start [shape=plaintext];\n");
while (instruction != null) {
Node node = mNodeMap.get(instruction);
if (node != null) {
if (node.successors != null) {
for (Node to : node.successors) {
sb.append(" ").append(getId(node)).append(" -> ").append(getId(to));
if (node.instruction instanceof JumpInsnNode) {
sb.append(" [label=\"");
if (((JumpInsnNode)node.instruction).label == to.instruction) {
sb.append("yes");
} else {
sb.append("no");
}
sb.append("\"]");
}
sb.append(";\n");
}
}
if (node.exceptions != null) {
for (Node to : node.exceptions) {
sb.append(getId(node)).append(" -> ").append(getId(to));
sb.append(" [label=\"exception\"];\n");
}
}
}
instruction = instruction.getNext();
}
// Labels
sb.append("\n");
for (Node node : mNodeMap.values()) {
instruction = node.instruction;
sb.append(" ").append(getId(node)).append(" ");
sb.append("[label=\"").append(dotDescribe(node)).append("\"");
if (highlight != null && highlight.contains(node)) {
sb.append(",shape=box,style=filled");
} else if (instruction instanceof LineNumberNode ||
instruction instanceof LabelNode ||
instruction instanceof FrameNode) {
sb.append(",shape=oval,style=dotted");
} else {
sb.append(",shape=box");
}
sb.append("];\n");
}
sb.append("}");
return sb.toString();
}
private static String dotDescribe(Node node) {
AbstractInsnNode instruction = node.instruction;
if (instruction instanceof LabelNode) {
return "Label";
} else if (instruction instanceof LineNumberNode) {
LineNumberNode lineNode = (LineNumberNode)instruction;
return "Line " + lineNode.line;
} else if (instruction instanceof FrameNode) {
return "Stack Frame";
} else if (instruction instanceof MethodInsnNode) {
MethodInsnNode method = (MethodInsnNode)instruction;
String cls = method.owner.substring(method.owner.lastIndexOf('/') + 1);
cls = cls.replace('$','.');
return "Call " + cls + "#" + method.name;
} else if (instruction instanceof FieldInsnNode) {
FieldInsnNode field = (FieldInsnNode) instruction;
String cls = field.owner.substring(field.owner.lastIndexOf('/') + 1);
cls = cls.replace('$','.');
return "Field " + cls + "#" + field.name;
} else if (instruction instanceof TypeInsnNode && instruction.getOpcode() == Opcodes.NEW) {
return "New " + ((TypeInsnNode)instruction).desc;
}
StringBuilder sb = new StringBuilder();
String opcodeName = getOpcodeName(instruction.getOpcode());
sb.append(opcodeName);
if (instruction instanceof IntInsnNode) {
IntInsnNode in = (IntInsnNode) instruction;
sb.append(" ").append(Integer.toString(in.operand));
} else if (instruction instanceof LdcInsnNode) {
LdcInsnNode ldc = (LdcInsnNode) instruction;
sb.append(" ");
if (ldc.cst instanceof String) {
sb.append("\\\"");
}
sb.append(ldc.cst);
if (ldc.cst instanceof String) {
sb.append("\\\"");
}
}
return sb.toString();
}
private static String getOpcodeName(int opcode) {
if (sOpcodeNames == null) {
sOpcodeNames = new String[255];
try {
Field[] fields = Opcodes.class.getDeclaredFields();
for (Field field : fields) {
if (field.getType() == int.class) {
String name = field.getName();
if (name.startsWith("ASM") || name.startsWith("V1_") ||
name.startsWith("ACC_") || name.startsWith("T_") ||
name.startsWith("H_") || name.startsWith("F_")) {
continue;
}
int val = field.getInt(null);
if (val >= 0 && val < sOpcodeNames.length) {
sOpcodeNames[val] = field.getName();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
if (opcode >= 0 && opcode < sOpcodeNames.length) {
String name = sOpcodeNames[opcode];
if (name != null) {
return name;
}
}
return Integer.toString(opcode);
}
private static String[] sOpcodeNames;
}