Last active
August 29, 2015 14:23
-
-
Save alangpierce/a35374aeb133caea27bc to your computer and use it in GitHub Desktop.
Java class that walks the Dagger dependencies in the KA code and generates a .dot file to use for a visual representation.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.khanacademy.android.graphgen; | |
import org.khanacademy.android.dependencies.components.ApplicationComponent; | |
import dagger.Component; | |
import dagger.Provides; | |
import java.io.FileNotFoundException; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.io.PrintWriter; | |
import java.lang.reflect.Constructor; | |
import java.lang.reflect.Field; | |
import java.lang.reflect.Method; | |
import java.util.HashSet; | |
import java.util.Set; | |
import javax.inject.Inject; | |
/** | |
* Hacked-together code to generate a DAG from the KA Android codebase. It does runtime reflection | |
* to walk the Dagger bindings and build the graph, and writes the result as a .dot file. | |
* | |
* Note that this code doesn't come close to supporting all of Dagger 2's features, and relies on the | |
* fact that the current KA code only uses a small subset of what Dagger 2 offers. For example, this | |
* code assumes that dependencies can be identified by a Class<?>, but Dagger 2 includes type parameters | |
* in the dependency name and supports @Named and any other qualifiers. | |
* | |
* Usage: | |
* If you haven't done so, run "brew install graphviz". | |
* Run this class. It generates an android.dot file. | |
* In a shell, run "cat android.dot | dot -Tpdf > android.pdf" This creates an android.pdf. | |
*/ | |
public class GraphGenerator { | |
private final Set<Class<?>> seenClasses = new HashSet<>(); | |
private final Set<Class<?>> seenProvides = new HashSet<>(); | |
private final PrintWriter writer; | |
public GraphGenerator(PrintWriter writer) { | |
this.writer = writer; | |
} | |
public static void main(String[] args) throws FileNotFoundException { | |
PrintWriter writer = new PrintWriter("/Users/apierce/graphviz/android.dot"); | |
writer.println("digraph G {"); | |
GraphGenerator graphGenerator = new GraphGenerator(writer); | |
graphGenerator.walkComponent(ApplicationComponent.class); | |
graphGenerator.emitAllClassInfo(); | |
writer.println("}"); | |
writer.flush(); | |
System.out.println("Done!"); | |
} | |
public void walkComponent(Class<?> componentClass) { | |
Component componentAnnotation = componentClass.getAnnotation(Component.class); | |
for (Class<?> module : componentAnnotation.modules()) { | |
walkModule(module); | |
} | |
for (Method method : componentClass.getDeclaredMethods()) { | |
if (method.getParameterTypes().length == 0) { | |
walkClass(method.getReturnType()); | |
} else { | |
walkClass(method.getParameterTypes()[0]); | |
} | |
} | |
} | |
public void walkModule(Class<?> module) { | |
for (Method method : module.getDeclaredMethods()) { | |
if (method.getAnnotation(Provides.class) != null) { | |
walkProvidesMethod(method); | |
} | |
} | |
writer.println(" subgraph cluster_" + module.getSimpleName() + " {"); | |
for (Method method : module.getDeclaredMethods()) { | |
if (method.getAnnotation(Provides.class) != null) { | |
emitModuleMembership(module, method.getReturnType()); | |
walkProvidesMethod(method); | |
} | |
} | |
writer.println(" }"); | |
writer.printf(" \"%s\" [shape=box fontsize=24]\n", module.getSimpleName()); | |
} | |
private void emitModuleMembership(Class<?> module, Class<?> returnType) { | |
writer.printf(" \"%s\" -> \"%s\";\n", module.getSimpleName(), returnType.getName()); | |
} | |
private void walkProvidesMethod(Method method) { | |
Class<?> returnType = method.getReturnType(); | |
if (seenProvides.contains(returnType)) { | |
return; | |
} | |
seenProvides.add(returnType); | |
walkClass(returnType); | |
for (Class<?> parameterType : method.getParameterTypes()) { | |
walkDependency(returnType, parameterType); | |
} | |
} | |
public void walkDependency(Class<?> fromClass, Class<?> toClass) { | |
emitEdge(fromClass, toClass); | |
walkClass(toClass); | |
} | |
public void walkClass(Class<?> dependencyClass) { | |
if (seenClasses.contains(dependencyClass)) { | |
return; | |
} | |
seenClasses.add(dependencyClass); | |
for (Constructor<?> constructor : dependencyClass.getDeclaredConstructors()) { | |
if (constructor.getAnnotation(Inject.class) != null) { | |
for (Class<?> parameterType : constructor.getParameterTypes()) { | |
walkDependency(dependencyClass, parameterType); | |
} | |
} | |
} | |
for (Field field : dependencyClass.getDeclaredFields()) { | |
if (field.getAnnotation(Inject.class) != null) { | |
walkDependency(dependencyClass, field.getType()); | |
} | |
} | |
for (Method method : dependencyClass.getDeclaredMethods()) { | |
if (method.getAnnotation(Inject.class) != null) { | |
for (Class<?> parameterType : method.getParameterTypes()) { | |
walkDependency(dependencyClass, parameterType); | |
} | |
} | |
} | |
} | |
public void emitEdge(Class<?> fromClass, Class<?> toClass) { | |
writer.printf(" \"%s\" -> \"%s\";%n", fromClass.getName(), toClass.getName()); | |
} | |
public void emitAllClassInfo() { | |
for (Class<?> seenClass : seenClasses) { | |
emitClassInfo(seenClass); | |
} | |
} | |
private void emitClassInfo(Class<?> classToEmit) { | |
int classSize = computeClassSize(classToEmit); | |
writer.printf(" \"%s\" [", classToEmit.getName()); | |
if (classToEmit.getName().startsWith("org.khanacademy.core")) { | |
writer.printf("penwidth=%s ", Math.log10(classSize)); | |
writer.printf("label=\"%s\" ", classToEmit.getSimpleName()); | |
writer.printf("fillcolor=lightblue "); | |
writer.printf("style=filled "); | |
writer.printf("fontsize=18 "); | |
} else if (classToEmit.getName().startsWith("org.khanacademy.android")) { | |
writer.printf("penwidth=%s ", Math.log10(classSize)); | |
writer.printf("label=\"%s\" ", classToEmit.getSimpleName()); | |
writer.printf("fontsize=18 "); | |
} else { | |
writer.printf("label=\"%s\\n%s\" ", | |
classToEmit.getPackage().getName(), classToEmit.getSimpleName()); | |
writer.printf("fillcolor=lightyellow "); | |
writer.printf("style=filled "); | |
} | |
writer.println("];"); | |
} | |
public int computeClassSize(Class<?> classToEmit) { | |
InputStream stream = classToEmit.getClassLoader() | |
.getResourceAsStream(classToEmit.getName().replace(".", "/") + ".class"); | |
int len = 0; | |
try { | |
while (stream.read() != -1) { | |
len++; | |
} | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
return len; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment