diff --git a/.gitignore b/.gitignore index cc5f15fa..4568c376 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ bin/ .project .settings/ +# intellij +.idea/ +*.iml + diff --git a/README.md b/README.md index 21d08e7e..b14037fe 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,11 @@ A suite of programs for generating static and dynamic call graphs in Java. #### Compile -The java-callgraph package is build with maven. Install maven and do: +The java-callgraph package is build with maven. Install maven and do: - +``` mvn install - +``` This will produce a `target` directory with the following three jars: - javacg-0.1-SNAPSHOT.jar: This is the standard maven packaged jar with static and dynamic call graph generator classes @@ -31,39 +31,42 @@ Instructions for running the callgraph generators `javacg-static` accepts as arguments the jars to analyze. - +``` java -jar javacg-0.1-SNAPSHOT-static.jar lib1.jar lib2.jar... - +``` `javacg-static` produces combined output in the following format: ###### For methods - - M:class1: (typeofcall)class2: - +``` + M:class1:(arg_types) (typeofcall)class2:(arg_types) +``` The line means that `method1` of `class1` called `method2` of `class2`. The type of call can have one of the following values (refer to -the [JVM specification](http://java.sun.com/docs/books/jvms/second_edition/html/Instructions2.doc6.html) -for the meaning of the calls): +the [JVM specification](http://java.sun.com/docs/books/jvms/second_edition/html/Instructions2.doc6.html) +for the meaning of the calls): * `M` for `invokevirtual` calls * `I` for `invokeinterface` calls * `O` for `invokespecial` calls * `S` for `invokestatic` calls + * `D` for `invokedynamic` calls + +For `invokedynamic` calls, it is not possible to infer the argument types. ###### For classes - +``` C:class1 class2 - +``` This means that some method(s) in `class1` called some method(s) in `class2`. ##### Dynamic -`javacg-dynamic` uses +`javacg-dynamic` uses [javassist](http://www.csg.is.titech.ac.jp/~chiba/javassist/) to insert probes at method entry and exit points. To be able to analyze a class `javassist` must resolve all dependent classes at instrumentation time. To do so, it reads @@ -81,7 +84,9 @@ entries), `javacg-dynamic` includes support for restricting the set of classes to be instrumented through include and exclude statements. The options are appended to the `-javaagent` argument and has the following format --javaagent:javacg-dycg-agent.jar="incl=mylib.*,mylib2.*,java.nio.*;excl=java.nio.charset.*" +``` +-javaagent:javacg-dycg-agent.jar="incl=mylib.*,mylib2.*,java.nio.*;excl=java.nio.charset.*" +``` The example above will instrument all classes under the the `mylib`, `mylib2` and `java.nio` namespaces, except those that fall under the `java.nio.charset` namespace. @@ -90,7 +95,7 @@ The example above will instrument all classes under the the `mylib`, `mylib2` an java -Xbootclasspath:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Classes/classes.jar:mylib.jar -javaagent:javacg-0.1-SNAPSHOT-dycg-agent.jar="incl=mylib.*;" --classpath mylib.jar mylib.Mainclass +-classpath mylib.jar mylib.Mainclass ``` `javacg-dynamic` produces two kinds of output. On the standard output, it @@ -110,16 +115,16 @@ a poor man's profiler. The format is the following: The output line starts with a `<` or `>` depending on whether it is a method entry or exit. It then writes the stack depth, thread id and the class and -method name, followed by a timestamp. The provided `process_trace.rb` +method name, followed by a timestamp. The provided `process_trace.rb` script processes the callgraph output to generate total time per method -information. +information. #### Examples -The following examples instrument the -[Dacapo benchmark suite](http://dacapobench.org/) to produce dynamic call graphs. +The following examples instrument the +[Dacapo benchmark suite](http://dacapobench.org/) to produce dynamic call graphs. The Dacapo benchmarks come in a single big jar archive that contains all dependency -libraries. To build the boot class path required for the javacg-dyn program, +libraries. To build the boot class path required for the javacg-dyn program, extract the `dacapo.jar` to a directory: all the required libraries can be found in the `jar` directory. @@ -167,7 +172,7 @@ org.apache.lucene.analysis.Token:termLength org.apache.lucene.analysis.Token:ini * The static call graph generator does not account for methods invoked via reflection. -* The dynamic call graph generator will not work reliably (or at all) for +* The dynamic call graph generator will not work reliably (or at all) for multithreaded programs * The dynamic call graph generator does not handle exceptions very well, so some methods might appear as having never returned @@ -179,4 +184,3 @@ Georgios Gousios #### License [2-clause BSD](http://www.opensource.org/licenses/bsd-license.php) - diff --git a/pom.xml b/pom.xml index 21046a41..a606ecad 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ org.apache.bcel bcel - 5.2 + 6.2 provided @@ -26,13 +26,31 @@ 3.12.1.GA provided + + io.cucumber + cucumber-java + 2.3.1 + test + + + io.cucumber + cucumber-junit + 2.3.1 + test + + + junit + junit + 4.12 + test + org.apache.maven.plugins maven-assembly-plugin - 2.2.1 + 3.0.0 assembly-dyn.xml @@ -58,11 +76,11 @@ maven-compiler-plugin - 2.3.2 + 3.6.1 false - 1.6 - 1.6 + 1.8 + 1.8 diff --git a/src/main/java/gr/gousiosg/javacg/dyn/Instrumenter.java b/src/main/java/gr/gousiosg/javacg/dyn/Instrumenter.java index 3aee29b8..2fbc698c 100644 --- a/src/main/java/gr/gousiosg/javacg/dyn/Instrumenter.java +++ b/src/main/java/gr/gousiosg/javacg/dyn/Instrumenter.java @@ -46,8 +46,8 @@ public class Instrumenter implements ClassFileTransformer { - static List pkgIncl = new ArrayList(); - static List pkgExcl = new ArrayList(); + static List pkgIncl = new ArrayList<>(); + static List pkgExcl = new ArrayList<>(); public static void premain(String argument, Instrumentation instrumentation) { @@ -63,14 +63,14 @@ public static void premain(String argument, Instrumentation instrumentation) { String[] tokens = argument.split(";"); if (tokens.length < 1) { - err("Missing delimeter ;"); + err("Missing delimiter ;"); return; } for (String token : tokens) { String[] args = token.split("="); if (args.length < 2) { - err("Missing argument delimeter =:" + token); + err("Missing argument delimiter =:" + token); return; } @@ -101,7 +101,7 @@ public static void premain(String argument, Instrumentation instrumentation) { instrumentation.addTransformer(new Instrumenter()); } - public byte[] transform(ClassLoader loader, String className, Class clazz, + public byte[] transform(ClassLoader loader, String className, Class clazz, java.security.ProtectionDomain domain, byte[] bytes) { boolean enhanceClass = false; @@ -118,7 +118,7 @@ public byte[] transform(ClassLoader loader, String className, Class clazz, for (Pattern p : pkgExcl) { Matcher m = p.matcher(name); if (m.matches()) { - err("Not enhansing class: " + name); + err("Skipping class: " + name); enhanceClass = false; break; } diff --git a/src/main/java/gr/gousiosg/javacg/dyn/MethodStack.java b/src/main/java/gr/gousiosg/javacg/dyn/MethodStack.java index 90cc7a0f..1da646dc 100644 --- a/src/main/java/gr/gousiosg/javacg/dyn/MethodStack.java +++ b/src/main/java/gr/gousiosg/javacg/dyn/MethodStack.java @@ -33,7 +33,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -41,32 +40,29 @@ public class MethodStack { - private static Stack stack = new Stack(); - private static Map, Integer> callgraph = new HashMap, Integer>(); + private static Stack stack = new Stack<>(); + private static Map, Integer> callgraph = new HashMap<>(); static FileWriter fw; static StringBuffer sb; static long threadid = -1L; - + static { Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { try { fw.close(); } catch (IOException e) { - // TODO Auto-generated catch block e.printStackTrace(); } //Sort by number of calls - List> keys = new ArrayList>(); + List> keys = new ArrayList<>(); keys.addAll(callgraph.keySet()); - Collections.sort(keys, new Comparator() { - public int compare(Object o1, Object o2) { - Integer v1 = callgraph.get(o1); - Integer v2 = callgraph.get(o2); - return v1.compareTo(v2); - } + Collections.sort(keys, (o1, o2) -> { + Integer v1 = callgraph.get(o1); + Integer v2 = callgraph.get(o2); + return v1.compareTo(v2); }); - + for (Pair key : keys) { System.out.println(key + " " + callgraph.get(key)); } @@ -89,7 +85,7 @@ public static void push(String callname) throws IOException { return; if (!stack.isEmpty()) { - Pair p = new Pair(stack.peek(), callname); + Pair p = new Pair<>(stack.peek(), callname); if (callgraph.containsKey(p)) callgraph.put(p, callgraph.get(p) + 1); else diff --git a/src/main/java/gr/gousiosg/javacg/dyn/Pair.java b/src/main/java/gr/gousiosg/javacg/dyn/Pair.java index 2328b075..c3315c1b 100644 --- a/src/main/java/gr/gousiosg/javacg/dyn/Pair.java +++ b/src/main/java/gr/gousiosg/javacg/dyn/Pair.java @@ -1,3 +1,31 @@ +/* + * Copyright (c) 2011 - Georgios Gousios + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + package gr.gousiosg.javacg.dyn; public class Pair { @@ -27,7 +55,7 @@ public boolean equals(Object obj) { if (obj.getClass() != getClass()) return false; - Pair p = (Pair)obj; + Pair p = (Pair)obj; return first.equals(p.first) && second.equals(p.second); } diff --git a/src/main/java/gr/gousiosg/javacg/stat/ClassVisitor.java b/src/main/java/gr/gousiosg/javacg/stat/ClassVisitor.java index e03d72c6..7e6d63d4 100644 --- a/src/main/java/gr/gousiosg/javacg/stat/ClassVisitor.java +++ b/src/main/java/gr/gousiosg/javacg/stat/ClassVisitor.java @@ -36,6 +36,9 @@ import org.apache.bcel.generic.ConstantPoolGen; import org.apache.bcel.generic.MethodGen; +import java.util.ArrayList; +import java.util.List; + /** * The simplest of class visitors, invokes the method visitor class for each * method found. @@ -45,7 +48,9 @@ public class ClassVisitor extends EmptyVisitor { private JavaClass clazz; private ConstantPoolGen constants; private String classReferenceFormat; - + private final DynamicCallManager DCManager = new DynamicCallManager(); + private List methodCalls = new ArrayList<>(); + public ClassVisitor(JavaClass jc) { clazz = jc; constants = new ConstantPoolGen(clazz.getConstantPool()); @@ -55,8 +60,13 @@ public ClassVisitor(JavaClass jc) { public void visitJavaClass(JavaClass jc) { jc.getConstantPool().accept(this); Method[] methods = jc.getMethods(); - for (int i = 0; i < methods.length; i++) - methods[i].accept(this); + for (int i = 0; i < methods.length; i++) { + Method method = methods[i]; + DCManager.retrieveCalls(method, jc); + DCManager.linkCalls(method); + method.accept(this); + + } } public void visitConstantPool(ConstantPool constantPool) { @@ -67,8 +77,7 @@ public void visitConstantPool(ConstantPool constantPool) { if (constant.getTag() == 7) { String referencedClass = constantPool.constantToString(constant); - System.out.println(String.format(classReferenceFormat, - referencedClass)); + System.out.println(String.format(classReferenceFormat, referencedClass)); } } } @@ -76,10 +85,15 @@ public void visitConstantPool(ConstantPool constantPool) { public void visitMethod(Method method) { MethodGen mg = new MethodGen(method, clazz.getClassName(), constants); MethodVisitor visitor = new MethodVisitor(mg, clazz); - visitor.start(); + methodCalls.addAll(visitor.start()); } - public void start() { + public ClassVisitor start() { visitJavaClass(clazz); + return this; + } + + public List methodCalls() { + return this.methodCalls; } } diff --git a/src/main/java/gr/gousiosg/javacg/stat/DynamicCallManager.java b/src/main/java/gr/gousiosg/javacg/stat/DynamicCallManager.java new file mode 100644 index 00000000..2c4e8813 --- /dev/null +++ b/src/main/java/gr/gousiosg/javacg/stat/DynamicCallManager.java @@ -0,0 +1,125 @@ +/* + * Written in 2018 by Matthieu Vergne + * + * To the extent possible under law, the author(s) have dedicated all + * copyright and related and neighboring rights to this software to + * the public domain worldwide. This software is distributed without + * any warranty. + * + * You should have received a copy of the CC0 Public Domain Dedication + * along with this software. If not, + * see . + */ + +package gr.gousiosg.javacg.stat; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.bcel.classfile.Attribute; +import org.apache.bcel.classfile.BootstrapMethod; +import org.apache.bcel.classfile.BootstrapMethods; +import org.apache.bcel.classfile.ConstantCP; +import org.apache.bcel.classfile.ConstantMethodHandle; +import org.apache.bcel.classfile.ConstantNameAndType; +import org.apache.bcel.classfile.ConstantPool; +import org.apache.bcel.classfile.ConstantUtf8; +import org.apache.bcel.classfile.JavaClass; +import org.apache.bcel.classfile.Method; + +/** + * {@link DynamicCallManager} provides facilities to retrieve information about + * dynamic calls statically. + *

+ * Most of the time, call relationships are explicit, which allows to properly + * build the call graph statically. But in the case of dynamic linking, i.e. + * invokedynamic instructions, this relationship might be unknown + * until the code is actually executed. Indeed, bootstrap methods are used to + * dynamically link the code at first call. One can read details about the + * invokedynamic + * instruction to know more about this mechanism. + *

+ * Nested lambdas are particularly subject to such absence of concrete caller, + * which lead us to produce method names like lambda$null$0, which + * breaks the call graph. This information can however be retrieved statically + * through the code of the bootstrap method called. + *

+ * In {@link #retrieveCalls(Method, JavaClass)}, we retrieve the (called, + * caller) relationships by analyzing the code of the caller {@link Method}. + * This information is then used in {@link #linkCalls(Method)} to rename the + * called {@link Method} properly. + * + * @author Matthieu Vergne + */ +public class DynamicCallManager { + private static final Pattern BOOTSTRAP_CALL_PATTERN = Pattern + .compile("invokedynamic\t(\\d+):\\S+ \\S+ \\(\\d+\\)"); + private static final int CALL_HANDLE_INDEX_ARGUMENT = 1; + + private final Map dynamicCallers = new HashMap<>(); + + /** + * Retrieve dynamic call relationships based on the code of the provided + * {@link Method}. + * + * @param method {@link Method} to analyze the code + * @param jc {@link JavaClass} info, which contains the bootstrap methods + * @see #linkCalls(Method) + */ + public void retrieveCalls(Method method, JavaClass jc) { + if (method.isAbstract() || method.isNative()) { + // No code to consider + return; + } + ConstantPool cp = method.getConstantPool(); + BootstrapMethod[] boots = getBootstrapMethods(jc); + String code = method.getCode().toString(); + Matcher matcher = BOOTSTRAP_CALL_PATTERN.matcher(code); + while (matcher.find()) { + int bootIndex = Integer.parseInt(matcher.group(1)); + BootstrapMethod bootMethod = boots[bootIndex]; + int calledIndex = bootMethod.getBootstrapArguments()[CALL_HANDLE_INDEX_ARGUMENT]; + String calledName = getMethodNameFromHandleIndex(cp, calledIndex); + String callerName = method.getName(); + dynamicCallers.put(calledName, callerName); + } + } + + private String getMethodNameFromHandleIndex(ConstantPool cp, int callIndex) { + ConstantMethodHandle handle = (ConstantMethodHandle) cp.getConstant(callIndex); + ConstantCP ref = (ConstantCP) cp.getConstant(handle.getReferenceIndex()); + ConstantNameAndType nameAndType = (ConstantNameAndType) cp.getConstant(ref.getNameAndTypeIndex()); + return nameAndType.getName(cp); + } + + /** + * Link the {@link Method}'s name to its concrete caller if required. + * + * @param method {@link Method} to analyze + * @see #retrieveCalls(Method, JavaClass) + */ + public void linkCalls(Method method) { + int nameIndex = method.getNameIndex(); + ConstantPool cp = method.getConstantPool(); + String methodName = ((ConstantUtf8) cp.getConstant(nameIndex)).getBytes(); + String linkedName = methodName; + String callerName = methodName; + while (linkedName.matches("(lambda\\$)+null(\\$\\d+)+")) { + callerName = dynamicCallers.get(callerName); + linkedName = linkedName.replace("null", callerName); + } + cp.setConstant(nameIndex, new ConstantUtf8(linkedName)); + } + + private BootstrapMethod[] getBootstrapMethods(JavaClass jc) { + for (Attribute attribute : jc.getAttributes()) { + if (attribute instanceof BootstrapMethods) { + return ((BootstrapMethods) attribute).getBootstrapMethods(); + } + } + return new BootstrapMethod[]{}; + } +} diff --git a/src/main/java/gr/gousiosg/javacg/stat/JCallGraph.java b/src/main/java/gr/gousiosg/javacg/stat/JCallGraph.java index 4d19408d..cb40dc96 100644 --- a/src/main/java/gr/gousiosg/javacg/stat/JCallGraph.java +++ b/src/main/java/gr/gousiosg/javacg/stat/JCallGraph.java @@ -28,48 +28,63 @@ package gr.gousiosg.javacg.stat; -import java.io.File; -import java.io.IOException; -import java.util.Enumeration; +import java.io.*; +import java.util.*; +import java.util.function.Function; import java.util.jar.JarEntry; import java.util.jar.JarFile; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import org.apache.bcel.classfile.ClassParser; /** * Constructs a callgraph out of a JAR archive. Can combine multiple archives * into a single call graph. - * + * * @author Georgios Gousios - * */ public class JCallGraph { public static void main(String[] args) { - ClassParser cp; + + Function getClassVisitor = + (ClassParser cp) -> { + try { + return new ClassVisitor(cp.parse()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }; + try { for (String arg : args) { File f = new File(arg); - + if (!f.exists()) { System.err.println("Jar file " + arg + " does not exist"); } - - JarFile jar = new JarFile(f); - Enumeration entries = jar.entries(); - while (entries.hasMoreElements()) { - JarEntry entry = entries.nextElement(); - if (entry.isDirectory()) - continue; + try (JarFile jar = new JarFile(f)) { + Stream entries = enumerationAsStream(jar.entries()); - if (!entry.getName().endsWith(".class")) - continue; + String methodCalls = entries. + flatMap(e -> { + if (e.isDirectory() || !e.getName().endsWith(".class")) + return (new ArrayList()).stream(); - cp = new ClassParser(arg,entry.getName()); - ClassVisitor visitor = new ClassVisitor(cp.parse()); - visitor.start(); + ClassParser cp = new ClassParser(arg, e.getName()); + return getClassVisitor.apply(cp).start().methodCalls().stream(); + }). + map(s -> s + "\n"). + reduce(new StringBuilder(), + StringBuilder::append, + StringBuilder::append).toString(); + + BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out)); + log.write(methodCalls); + log.close(); } } } catch (IOException e) { @@ -77,4 +92,19 @@ public static void main(String[] args) { e.printStackTrace(); } } + + public static Stream enumerationAsStream(Enumeration e) { + return StreamSupport.stream( + Spliterators.spliteratorUnknownSize( + new Iterator() { + public T next() { + return e.nextElement(); + } + + public boolean hasNext() { + return e.hasMoreElements(); + } + }, + Spliterator.ORDERED), false); + } } diff --git a/src/main/java/gr/gousiosg/javacg/stat/MethodVisitor.java b/src/main/java/gr/gousiosg/javacg/stat/MethodVisitor.java index 9f233ccd..21100420 100644 --- a/src/main/java/gr/gousiosg/javacg/stat/MethodVisitor.java +++ b/src/main/java/gr/gousiosg/javacg/stat/MethodVisitor.java @@ -29,18 +29,11 @@ package gr.gousiosg.javacg.stat; import org.apache.bcel.classfile.JavaClass; -import org.apache.bcel.generic.ConstantPoolGen; -import org.apache.bcel.generic.ConstantPushInstruction; -import org.apache.bcel.generic.EmptyVisitor; -import org.apache.bcel.generic.INVOKEINTERFACE; -import org.apache.bcel.generic.INVOKESPECIAL; -import org.apache.bcel.generic.INVOKESTATIC; -import org.apache.bcel.generic.INVOKEVIRTUAL; -import org.apache.bcel.generic.Instruction; -import org.apache.bcel.generic.InstructionConstants; -import org.apache.bcel.generic.InstructionHandle; -import org.apache.bcel.generic.MethodGen; -import org.apache.bcel.generic.ReturnInstruction; +import org.apache.bcel.generic.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * The simplest of method visitors, prints any invoked method @@ -54,18 +47,31 @@ public class MethodVisitor extends EmptyVisitor { private MethodGen mg; private ConstantPoolGen cp; private String format; + private List methodCalls = new ArrayList<>(); public MethodVisitor(MethodGen m, JavaClass jc) { visitedClass = jc; mg = m; cp = mg.getConstantPool(); - format = "M:" + visitedClass.getClassName() + ":" + mg.getName() - + " " + "(%s)%s:%s"; + format = "M:" + visitedClass.getClassName() + ":" + mg.getName() + "(" + argumentList(mg.getArgumentTypes()) + ")" + + " " + "(%s)%s:%s(%s)"; } - public void start() { + private String argumentList(Type[] arguments) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < arguments.length; i++) { + if (i != 0) { + sb.append(","); + } + sb.append(arguments[i].toString()); + } + return sb.toString(); + } + + public List start() { if (mg.isAbstract() || mg.isNative()) - return; + return Collections.emptyList(); + for (InstructionHandle ih = mg.getInstructionList().getStart(); ih != null; ih = ih.getNext()) { Instruction i = ih.getInstruction(); @@ -73,33 +79,39 @@ public void start() { if (!visitInstruction(i)) i.accept(this); } + return methodCalls; } private boolean visitInstruction(Instruction i) { short opcode = i.getOpcode(); - - return ((InstructionConstants.INSTRUCTIONS[opcode] != null) + return ((InstructionConst.getInstruction(opcode) != null) && !(i instanceof ConstantPushInstruction) && !(i instanceof ReturnInstruction)); } @Override public void visitINVOKEVIRTUAL(INVOKEVIRTUAL i) { - System.out.println(String.format(format,"M",i.getReferenceType(cp),i.getMethodName(cp))); + methodCalls.add(String.format(format,"M",i.getReferenceType(cp),i.getMethodName(cp),argumentList(i.getArgumentTypes(cp)))); } @Override public void visitINVOKEINTERFACE(INVOKEINTERFACE i) { - System.out.println(String.format(format,"I",i.getReferenceType(cp),i.getMethodName(cp))); + methodCalls.add(String.format(format,"I",i.getReferenceType(cp),i.getMethodName(cp),argumentList(i.getArgumentTypes(cp)))); } @Override public void visitINVOKESPECIAL(INVOKESPECIAL i) { - System.out.println(String.format(format,"O",i.getReferenceType(cp),i.getMethodName(cp))); + methodCalls.add(String.format(format,"O",i.getReferenceType(cp),i.getMethodName(cp),argumentList(i.getArgumentTypes(cp)))); } @Override public void visitINVOKESTATIC(INVOKESTATIC i) { - System.out.println(String.format(format,"S",i.getReferenceType(cp),i.getMethodName(cp))); + methodCalls.add(String.format(format,"S",i.getReferenceType(cp),i.getMethodName(cp),argumentList(i.getArgumentTypes(cp)))); + } + + @Override + public void visitINVOKEDYNAMIC(INVOKEDYNAMIC i) { + methodCalls.add(String.format(format,"D",i.getType(cp),i.getMethodName(cp), + argumentList(i.getArgumentTypes(cp)))); } } diff --git a/src/test/java/gr/gousiosg/javacg/JARBuilder.java b/src/test/java/gr/gousiosg/javacg/JARBuilder.java new file mode 100644 index 00000000..327c55b1 --- /dev/null +++ b/src/test/java/gr/gousiosg/javacg/JARBuilder.java @@ -0,0 +1,117 @@ +package gr.gousiosg.javacg; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +import javax.tools.Diagnostic; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaCompiler.CompilationTask; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; + +public class JARBuilder { + private static final String TEMP_DIR = System.getProperty("java.io.tmpdir"); + + private final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + private final DiagnosticCollector diagnostics = new DiagnosticCollector(); + private final StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null); + private final LinkedList compilationUnits = new LinkedList<>(); + private final Collection classFiles = new LinkedList<>(); + + public JARBuilder() throws IOException { + fileManager.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(new File(TEMP_DIR))); + } + + public void add(String className, String classCode) throws IOException { + compilationUnits.add(createJavaFile(className, classCode)); + classFiles.add(new File(TEMP_DIR, className + ".class")); + } + + public File build() throws FileNotFoundException, IOException { + CompilationTask task = compiler.getTask(null, fileManager, diagnostics, null, null, compilationUnits); + boolean success = task.call(); + if (!success) { + displayDiagnostic(diagnostics); + throw new RuntimeException("Cannot compile classes for the JAR"); + } + + File file = File.createTempFile("test", ".jar"); + JarOutputStream jar = new JarOutputStream(new FileOutputStream(file), createManifest()); + for (File classFile : classFiles) { + add(classFile, jar); + } + jar.close(); + return file; + } + + private void displayDiagnostic(DiagnosticCollector diagnostics) { + for (Diagnostic diagnostic : diagnostics.getDiagnostics()) { + JavaSourceFromString sourceClass = (JavaSourceFromString) diagnostic.getSource(); + System.err.println("-----"); + System.err.println("Source: " + sourceClass.getName()); + System.err.println("Message: " + diagnostic.getMessage(null)); + System.err.println("Position: " + diagnostic.getPosition()); + System.err.println(diagnostic.getKind() + " " + diagnostic.getCode()); + } + } + + private JavaFileObject createJavaFile(String className, String classCode) throws IOException { + StringWriter writer = new StringWriter(); + writer.append(classCode); + writer.close(); + JavaFileObject file = new JavaSourceFromString(className, writer.toString()); + return file; + } + + private Manifest createManifest() { + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + return manifest; + } + + private void add(File classFile, JarOutputStream jar) throws IOException { + JarEntry entry = new JarEntry(classFile.getPath().replace("\\", "/")); + jar.putNextEntry(entry); + try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(classFile))) { + byte[] buffer = new byte[1024]; + while (true) { + int count = in.read(buffer); + if (count == -1) + break; + jar.write(buffer, 0, count); + } + jar.closeEntry(); + } + } +} + +class JavaSourceFromString extends SimpleJavaFileObject { + final String code; + + JavaSourceFromString(String name, String code) throws IOException { + super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); + this.code = code; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return code; + } +} \ No newline at end of file diff --git a/src/test/java/gr/gousiosg/javacg/RunCucumberTest.java b/src/test/java/gr/gousiosg/javacg/RunCucumberTest.java new file mode 100644 index 00000000..e99e65aa --- /dev/null +++ b/src/test/java/gr/gousiosg/javacg/RunCucumberTest.java @@ -0,0 +1,10 @@ +package gr.gousiosg.javacg; + +import cucumber.api.CucumberOptions; +import cucumber.api.junit.Cucumber; +import org.junit.runner.RunWith; + +@RunWith(Cucumber.class) +@CucumberOptions(plugin = { "pretty" }) +public class RunCucumberTest { +} \ No newline at end of file diff --git a/src/test/java/gr/gousiosg/javacg/StepDefinitions.java b/src/test/java/gr/gousiosg/javacg/StepDefinitions.java new file mode 100644 index 00000000..281c1c1d --- /dev/null +++ b/src/test/java/gr/gousiosg/javacg/StepDefinitions.java @@ -0,0 +1,48 @@ +package gr.gousiosg.javacg; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; + +import cucumber.api.java.en.Given; +import cucumber.api.java.en.Then; +import cucumber.api.java.en.When; +import gr.gousiosg.javacg.stat.JCallGraph; + +public class StepDefinitions { + private final JARBuilder jarBuilder; + private String result; + + public StepDefinitions() throws IOException { + jarBuilder = new JARBuilder(); + } + + @Given("^I have the class \"([^\"]*)\" with code:$") + public void i_have_the_class_with_code(String className, String classCode) throws Exception { + jarBuilder.add(className, classCode); + } + + @When("^I run the analyze$") + public void i_analyze_it() throws Exception { + File jarFile = jarBuilder.build(); + + PrintStream oldOut = System.out; + ByteArrayOutputStream resultBuffer = new ByteArrayOutputStream(); + System.setOut(new PrintStream(resultBuffer)); + JCallGraph.main(new String[] { jarFile.getPath() }); + System.setOut(oldOut); + + result = resultBuffer.toString(); + } + + @Then("^the result should contain:$") + public void the_result_should_contain(String line) throws Exception { + if (result.contains(line)) { + // OK + } else { + System.err.println(result); + throw new RuntimeException("Cannot found: " + line); + } + } +} diff --git a/src/test/resources/gr/gousiosg/javacg/lambda.feature b/src/test/resources/gr/gousiosg/javacg/lambda.feature new file mode 100644 index 00000000..c651e953 --- /dev/null +++ b/src/test/resources/gr/gousiosg/javacg/lambda.feature @@ -0,0 +1,77 @@ +#Author: matthieu.vergne@gmail.com +Feature: Lambda + I want to identify all lambdas within the analyzed code. + + Background: + # Introduce the lambda we will use + Given I have the class "Runner" with code: + """ + @FunctionalInterface + public interface Runner { + public void run(); + } + """ + + Scenario: Retrieve lambda in method + Given I have the class "LambdaTest" with code: + """ + public class LambdaTest { + public void methodA() { + Runner r = () -> methodB(); + r.run(); + } + + public void methodB() {} + } + """ + When I run the analyze + # Creation of r in methodA + Then the result should contain: + """ + M:LambdaTest:methodA() (D)Runner:run(LambdaTest) + """ + # Call of methodB in r + And the result should contain: + """ + M:LambdaTest:lambda$methodA$0() (M)LambdaTest:methodB() + """ + + Scenario: Retrieve nested lambdas + Given I have the class "NestedLambdaTest" with code: + """ + public class NestedLambdaTest { + public void methodA() { + Runner r = () -> { + Runner r2 = () -> { + Runner r3 = () -> methodB(); + r3.run(); + }; + r2.run(); + }; + r.run(); + } + + public void methodB() {} + } + """ + When I run the analyze + # Creation of r in methodA + Then the result should contain: + """ + M:NestedLambdaTest:methodA() (D)Runner:run(NestedLambdaTest) + """ + # Creation of r2 in r + And the result should contain: + """ + M:NestedLambdaTest:lambda$methodA$2() (D)Runner:run(NestedLambdaTest) + """ + # Creation of r3 in r2 + And the result should contain: + """ + M:NestedLambdaTest:lambda$lambda$methodA$2$1() (D)Runner:run(NestedLambdaTest) + """ + # Call of methodB in r3 + And the result should contain: + """ + M:NestedLambdaTest:lambda$lambda$lambda$methodA$2$1$0() (M)NestedLambdaTest:methodB() + """ diff --git a/src/test/resources/gr/gousiosg/javacg/native.feature b/src/test/resources/gr/gousiosg/javacg/native.feature new file mode 100644 index 00000000..877a69c9 --- /dev/null +++ b/src/test/resources/gr/gousiosg/javacg/native.feature @@ -0,0 +1,20 @@ +#Author: matthieu.vergne@gmail.com +Feature: Native + I want to identify all native methods within the analyzed code. + + Scenario: Retrieve native method call + Given I have the class "NativeTest" with code: + """ + public class NativeTest { + public void methodA() { + methodB(); + } + + public native void methodB(); + } + """ + When I run the analyze + Then the result should contain: + """ + M:NativeTest:methodA() (M)NativeTest:methodB() + """ \ No newline at end of file