This commit is contained in:
Claudio Maggioni 2023-11-18 17:26:15 +01:00
parent 8395b10029
commit 621a95e291
14 changed files with 567 additions and 127 deletions

View file

@ -1,5 +1,6 @@
package ch.usi.inf.sp.callgraph; package ch.usi.inf.sp.callgraph;
import ch.usi.inf.sp.callgraph.renderer.DotGraph;
import org.objectweb.asm.Opcodes; import org.objectweb.asm.Opcodes;
import java.io.FileWriter; import java.io.FileWriter;
@ -19,135 +20,12 @@ import java.util.stream.Collectors;
*/ */
public final class CallGraphRenderer { public final class CallGraphRenderer {
private final Map<String, Integer> identifiers = new HashMap<>();
private final Map<Integer, Boolean> nodeBuilt = new HashMap<>();
private String identifier(String what, boolean fromNode) {
return "node" + identifiers.computeIfAbsent(what, (k) -> {
int key = identifiers.size() + 1;
nodeBuilt.compute(key, (ke, v) -> v == null ? fromNode : (v || fromNode));
return key;
});
}
private String classNodeName(ClassType type) {
return identifier(type.getInternalName(), true);
}
private static String methodDescription(Method e) {
return (e.getVerboseModifiers() + " " +
e.getName().replaceAll("<", "&lt;").replaceAll(">", "&gt;") +
e.getDescriptor()).trim();
}
private static String classNodeDescriptor(ClassType type) {
final String className = type.getInternalName();
if (!type.isResolved()) {
return "[shape=ellipse,style=dashed,label=\"" + className + "\"]";
}
final String methods = type.getMethods().stream()
.map(CallGraphRenderer::methodDescription)
.collect(Collectors.joining("\\n"));
final String style = type.isAbstract() ? "dashed" : "solid";
return "[shape=record,style=" + style + ",label=\"{" + className + "|" + methods + "}\"]";
}
private String methodNodeName(ClassHierarchy hierarchy, String className, String name, String descriptor, boolean fromNode) {
try {
if (!hierarchy.getOrCreateClass(className).isResolved()) {
return identifier(className, true);
}
} catch (TypeInconsistencyException e) {
throw new RuntimeException(e);
}
return identifier(className + " " + name + " " + descriptor, fromNode);
}
private static String methodNodeDescriptor(Method method) {
return "[shape=rectangle,style=filled,fillcolor=lightgreen,label=\"" + method.getDeclaringClassName() +
"\\n" + methodDescription(method) + "\"]";
}
private static String getEdge(String from, String to, int opCode, String color) {
String style;
switch (opCode) {
case Opcodes.INVOKEVIRTUAL: style = "dashed"; break;
case Opcodes.INVOKEINTERFACE: style = "dotted"; break;
default: style = "solid";
}
return from + " -> " + to + " [style=" + style + ",color=" + color + "]";
}
public void dumpDot(final ClassHierarchy hierarchy, final String fileName) throws IOException { public void dumpDot(final ClassHierarchy hierarchy, final String fileName) throws IOException {
final DotGraph g = new DotGraph();
g.build(hierarchy);
final PrintWriter pw = new PrintWriter(new FileWriter(fileName)); final PrintWriter pw = new PrintWriter(new FileWriter(fileName));
pw.println("digraph CallGraph {"); pw.print(g.toDot());
pw.println(" rankdir=\"BT\"");
final ArrayList<String> edges = new ArrayList<>();
for (final Type type : hierarchy.getTypes()) {
if (type instanceof ClassType) {
final ClassType classType = (ClassType) type;
final ClassType superClassType = classType.getSuperClass();
pw.println(classNodeName(classType) + " " + classNodeDescriptor(classType) + ";");
if (superClassType != null) {
edges.add(getEdge(classNodeName(classType), classNodeName(superClassType), 0, "black"));
}
for (final ClassType c : classType.getInterfaces()) {
edges.add(getEdge(classNodeName(classType), classNodeName(c), Opcodes.INVOKEVIRTUAL, "black"));
}
for (final Method m : classType.getMethods()) {
final String mName = methodNodeName(hierarchy, m.getDeclaringClassName(), m.getName(), m.getDescriptor(), true);
pw.println(mName + " " + methodNodeDescriptor(m) + ";");
edges.add(getEdge(classNodeName(classType), mName, 0, "lightgreen"));
for (final CallSite c : m.getCallSites()) {
final String cName = methodNodeName(hierarchy,
c.getDeclaredTargetClassName(),
c.getTargetMethodName(),
c.getTargetMethodDescriptor(), false);
edges.add(getEdge(mName, cName, c.getOpcode(), "blue"));
for (final ClassType p : c.getPossibleTargetClasses()) {
final String pName = methodNodeName(hierarchy,
p.getInternalName(),
c.getTargetMethodName(),
c.getTargetMethodDescriptor(), false);
edges.add(getEdge(mName, pName, c.getOpcode(), "red"));
}
}
}
}
}
// Add missing node declarations (happens with methods in classes not scanned)
final Map<Integer, String> reverseIdentifiers = identifiers.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
for (Map.Entry<Integer, Boolean> e : nodeBuilt.entrySet()) {
if (!e.getValue()) {
pw.println("node" + e.getKey() + " [label=\"" + reverseIdentifiers.get(e.getKey()) + "\"]");
}
}
pw.append('\n');
for (final String edge : edges) {
pw.println(edge);
}
pw.println("}");
pw.close(); pw.close();
} }

View file

@ -0,0 +1,27 @@
package ch.usi.inf.sp.callgraph.renderer;
import ch.usi.inf.sp.callgraph.CallSite;
public class CallSiteAdapter extends MethodLike {
private final CallSite callSite;
public CallSiteAdapter(CallSite callSite) {
this.callSite = callSite;
}
@Override
public String getDeclaringClass() {
return callSite.getDeclaredTargetClassName();
}
@Override
public String getName() {
return callSite.getTargetMethodName();
}
@Override
public String getDescriptor() {
return callSite.getTargetMethodDescriptor();
}
}

View file

@ -0,0 +1,43 @@
package ch.usi.inf.sp.callgraph.renderer;
import java.util.Map;
import java.util.Objects;
public class ClassToMethodEdge implements DotEdge {
private final DotClassNode from;
private final DotMethodNode to;
public ClassToMethodEdge(DotClassNode from, DotMethodNode to) {
this.from = from;
this.to = to;
}
public DotClassNode getFrom() {
return from;
}
public DotMethodNode getTo() {
return to;
}
@Override
public Map<String, String> getProperties() {
return Map.of(
"style", "solid",
"color", "lightgreen"
);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ClassToMethodEdge that = (ClassToMethodEdge) o;
return Objects.equals(from, that.from) && Objects.equals(to, that.to);
}
@Override
public int hashCode() {
return Objects.hash(from, to);
}
}

View file

@ -0,0 +1,66 @@
package ch.usi.inf.sp.callgraph.renderer;
import ch.usi.inf.sp.callgraph.ClassType;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
public class DotClassNode implements DotNode {
private final ClassType type;
public DotClassNode(ClassType type) {
this.type = type;
}
public boolean isResolved() {
return type.isResolved();
}
public ClassType getType() {
return this.type;
}
public String getStyle() {
return type.isInterface() ? "dotted" : type.isAbstract() ? "dashed" : "solid";
}
@Override
public Map<String, String> getProperties() {
final String className = type.getInternalName();
if (!isResolved()) {
// "External" classes (like java.lang.Math) are just ellipsis
return Map.of(
"shape", "ellipse",
"style", getStyle(),
"label", className
);
}
final String methods = type.getMethods().stream()
.map(MethodAdapter::new)
.map(MethodLike::prettyName)
.collect(Collectors.joining("\\n"));
return Map.of(
"shape", "record",
"style", getStyle(),
"label", DotNode.recordSplit(className, methods)
);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DotClassNode that = (DotClassNode) o;
return Objects.equals(type, that.type);
}
@Override
public int hashCode() {
return Objects.hash(type);
}
}

View file

@ -0,0 +1,16 @@
package ch.usi.inf.sp.callgraph.renderer;
import java.util.Map;
public interface DotEdge {
DotNode getFrom();
DotNode getTo();
Map<String, String> getProperties();
default String getConnector() {
return "->";
}
}

View file

@ -0,0 +1,132 @@
package ch.usi.inf.sp.callgraph.renderer;
import ch.usi.inf.sp.callgraph.*;
import java.util.*;
import java.util.stream.Collectors;
public class DotGraph {
// use map[e->e] instead of set[e] to allow fetching equal element
private final DuplicateAwareSet<DotClassNode> classNodes = new DuplicateAwareSet<>();
private final DuplicateAwareSet<DotMethodNode> methodNodes = new DuplicateAwareSet<>();
private final List<DotEdge> otherEdges = new ArrayList<>();
private final Set<DotEdge> classToMethodEdges = new HashSet<>();
private static String formatProperties(Map<String, String> properties) {
return properties.entrySet().stream()
.map((e) -> String.format("%s=\"%s\"", e.getKey(), e.getValue()))
.collect(Collectors.joining(","));
}
private static <E> Map<E, Integer> enumerate(Set<E> set) {
final Map<E, Integer> map = new HashMap<>();
int index = 0;
for (final E element : set) {
map.put(element, index++);
}
return Collections.unmodifiableMap(map);
}
private DotClassNode considerClass(final ClassType classType) {
return classNodes.addIfNew(new DotClassNode(classType), (addedClassNode) -> {
if (addedClassNode.isResolved()) {
for (final Method m : classType.getMethods()) {
methodNodes.addIfNew(
new DotMethodNode(new MethodAdapter(m)),
(addedMethodNode) -> classToMethodEdges.add(new ClassToMethodEdge(addedClassNode, addedMethodNode))
);
}
for (final ClassType c : classType.getInterfaces()) {
final DotClassNode interfaceNode = considerClass(c);
otherEdges.add(new TypeHierarchyEdge(addedClassNode, interfaceNode));
}
}
});
}
private void scanMethods() {
final Deque<DotMethodNode> methodQueue = new ArrayDeque<>(methodNodes.getAll());
while (!methodQueue.isEmpty()) {
final DotMethodNode methodNode = methodQueue.pop();
final Method m = methodNode.getMethodLike().getMethod();
if (m == null) continue; // we care about actual methods only, not callsites
for (final CallSite c : m.getCallSites()) {
final DotMethodNode toMethodNode = methodNodes.addIfNew(new DotMethodNode(new CallSiteAdapter(c)));
otherEdges.add(new InvokeEdge(methodNode, toMethodNode, c.getOpcode(), true));
for (final ClassType p : c.getPossibleTargetClasses()) {
if (p.getInternalName().endsWith("Math")) {
System.out.println("aaa");
}
final DotMethodNode pm = methodNodes.addIfNew(new DotMethodNode(new PossibleTargetAdapter(c, p)));
final DotClassNode pc = classNodes.addIfNew(new DotClassNode(p));
// if the target is a new node, the class of that node might not have been analyzed,
// thus class to method edges might not have been added
classToMethodEdges.add(new ClassToMethodEdge(pc, pm));
}
otherEdges.add(new InvokeEdge(methodNode, toMethodNode, c.getOpcode(), false));
}
}
}
public void build(final ClassHierarchy hierarchy) {
for (final Type type : hierarchy.getTypes()) {
if (type instanceof ClassType) {
final ClassType classType = (ClassType) type;
final DotClassNode node = considerClass(classType);
final ClassType superClassType = classType.getSuperClass();
if (superClassType != null) {
final DotClassNode superNode = considerClass(superClassType);
otherEdges.add(new TypeHierarchyEdge(node, superNode));
}
}
}
scanMethods();
}
public String toDot() {
final StringBuilder s = new StringBuilder();
s.append("digraph CallGraph {\n");
s.append("rankdir=\"BT\"\n");
final Set<DotNode> nodes = new HashSet<>();
nodes.addAll(classNodes.getAll());
nodes.addAll(methodNodes.getAll());
final Map<DotNode, Integer> nodeToId = enumerate(nodes);
for (final Map.Entry<DotNode, Integer> e : nodeToId.entrySet()) {
s.append(buildNode(e.getKey(), e.getValue()));
}
s.append('\n');
final List<DotEdge> edges = new ArrayList<>(otherEdges);
edges.addAll(classToMethodEdges);
for (final DotEdge e : edges) {
s.append(buildEdge(e, nodeToId.get(e.getFrom()), nodeToId.get(e.getTo())));
}
s.append("}\n");
return s.toString();
}
private String buildNode(DotNode node, int nodeId) {
return String.format("node%d [%s];\n", nodeId, formatProperties(node.getProperties()));
}
private String buildEdge(DotEdge edge, int idFrom, int idTo) {
return String.format("node%d %s node%d [%s];\n", idFrom, edge.getConnector(), idTo, formatProperties(edge.getProperties()));
}
}

View file

@ -0,0 +1,41 @@
package ch.usi.inf.sp.callgraph.renderer;
import java.util.Map;
import java.util.Objects;
public class DotMethodNode implements DotNode {
private static final String METHOD_COLOR = "lightgreen";
private final MethodLike method;
public DotMethodNode(MethodLike method) {
this.method = method;
}
public MethodLike getMethodLike() {
return method;
}
@Override
public Map<String, String> getProperties() {
return Map.of(
"shape", "rectangle",
"style", "filled",
"fillcolor", METHOD_COLOR,
"label", String.format("%s\\n%s", method.getDeclaringClass(), method.prettyName())
);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DotMethodNode that = (DotMethodNode) o;
return Objects.equals(method, that.method);
}
@Override
public int hashCode() {
return Objects.hash(method);
}
}

View file

@ -0,0 +1,15 @@
package ch.usi.inf.sp.callgraph.renderer;
import ch.usi.inf.sp.callgraph.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
public interface DotNode {
static String recordSplit(String... elements) {
return Arrays.stream(elements).collect(Collectors.joining("|", "{", "}"));
}
Map<String, String> getProperties();
}

View file

@ -0,0 +1,32 @@
package ch.usi.inf.sp.callgraph.renderer;
import java.util.*;
import java.util.function.Consumer;
public class DuplicateAwareSet<E> {
private final Map<E, E> elementsToElements = new HashMap<>();
public E addIfNew(final E element) {
if (elementsToElements.containsKey(element)) {
return elementsToElements.get(element);
}
elementsToElements.put(element, element);
return element;
}
public E addIfNew(final E element, Consumer<E> ifNew) {
final E added = addIfNew(element);
if (element == added) {
ifNew.accept(element);
}
return added;
}
public Set<E> getAll() {
return new HashSet<>(elementsToElements.keySet());
}
}

View file

@ -0,0 +1,60 @@
package ch.usi.inf.sp.callgraph.renderer;
import org.objectweb.asm.Opcodes;
import java.util.Map;
public class InvokeEdge implements DotEdge {
private final DotMethodNode from;
private final DotMethodNode to;
private final int invokeOpCode;
private final boolean isDeclared;
public InvokeEdge(DotMethodNode from, DotMethodNode to, int invokeOpCode, boolean isDeclared) {
if (invokeOpCode != Opcodes.INVOKESTATIC &&
invokeOpCode != Opcodes.INVOKESPECIAL &&
invokeOpCode != Opcodes.INVOKEVIRTUAL &&
invokeOpCode != Opcodes.INVOKEINTERFACE) {
throw new IllegalArgumentException("invokeOpCode must be a invoke instruction opcode");
}
this.from = from;
this.to = to;
this.invokeOpCode = invokeOpCode;
this.isDeclared = isDeclared;
}
private String getStyle() {
switch (invokeOpCode) {
case Opcodes.INVOKESTATIC:
case Opcodes.INVOKESPECIAL:
return "solid";
case Opcodes.INVOKEVIRTUAL:
return "dashed";
case Opcodes.INVOKEINTERFACE:
return "dotted";
default:
throw new IllegalStateException("unreachable");
}
}
@Override
public Map<String, String> getProperties() {
return Map.of(
"style", getStyle(),
"color", isDeclared ? "blue" : "red"
);
}
public DotMethodNode getFrom() {
return from;
}
public DotMethodNode getTo() {
return to;
}
}

View file

@ -0,0 +1,39 @@
package ch.usi.inf.sp.callgraph.renderer;
import ch.usi.inf.sp.callgraph.Method;
import java.util.Objects;
public class MethodAdapter extends MethodLike {
private final Method method;
public MethodAdapter(Method method) {
this.method = method;
}
@Override
public Method getMethod() {
return this.method;
}
@Override
public String getVerboseModifiers() {
return method.getVerboseModifiers();
}
@Override
public String getDeclaringClass() {
return method.getDeclaringClassName();
}
@Override
public String getName() {
return method.getName();
}
@Override
public String getDescriptor() {
return method.getDescriptor();
}
}

View file

@ -0,0 +1,43 @@
package ch.usi.inf.sp.callgraph.renderer;
import ch.usi.inf.sp.callgraph.Method;
import java.util.Objects;
public abstract class MethodLike {
public String getVerboseModifiers() {
return "";
}
public Method getMethod() {
return null;
}
public abstract String getDeclaringClass();
public abstract String getName();
public abstract String getDescriptor();
public String prettyName() {
return (getVerboseModifiers() + " " +
getName().replaceAll("<", "&lt;").replaceAll(">", "&gt;") +
getDescriptor()).trim();
}
private String toSerializedString() {
return String.format("%s.%s %s", getDeclaringClass(), getName(), getDescriptor());
}
@Override
public int hashCode() {
return Objects.hashCode(toSerializedString());
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof MethodLike)) return false;
final MethodLike m = (MethodLike) obj;
return Objects.equals(toSerializedString(), m.toSerializedString());
}
}

View file

@ -0,0 +1,19 @@
package ch.usi.inf.sp.callgraph.renderer;
import ch.usi.inf.sp.callgraph.CallSite;
import ch.usi.inf.sp.callgraph.ClassType;
public class PossibleTargetAdapter extends CallSiteAdapter {
private final ClassType possibleTarget;
public PossibleTargetAdapter(CallSite callSite, ClassType possibleTarget) {
super(callSite);
this.possibleTarget = possibleTarget;
}
@Override
public String getDeclaringClass() {
return possibleTarget.getInternalName();
}
}

View file

@ -0,0 +1,29 @@
package ch.usi.inf.sp.callgraph.renderer;
import java.util.Map;
public class TypeHierarchyEdge implements DotEdge {
private final DotClassNode subClass;
private final DotClassNode superClass;
public TypeHierarchyEdge(DotClassNode subClass, DotClassNode superClass) {
this.subClass = subClass;
this.superClass = superClass;
}
public DotClassNode getFrom() {
return subClass;
}
public DotClassNode getTo() {
return superClass;
}
@Override
public Map<String, String> getProperties() {
return Map.of(
"style", superClass.getStyle(),
"color", "black"
);
}
}