1 /* 2 * Copyright (C) 2017 The Dagger Authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package dagger.example.spi; 18 19 import static java.util.UUID.randomUUID; 20 import static java.util.regex.Matcher.quoteReplacement; 21 import static java.util.stream.Collectors.groupingBy; 22 23 import com.google.auto.service.AutoService; 24 import com.google.common.base.Joiner; 25 import com.google.common.base.Strings; 26 import com.google.common.collect.ImmutableList; 27 import com.google.common.collect.Iterators; 28 import com.google.common.graph.EndpointPair; 29 import com.google.errorprone.annotations.CanIgnoreReturnValue; 30 import com.squareup.javapoet.ClassName; 31 import dagger.model.Binding; 32 import dagger.model.BindingGraph; 33 import dagger.model.BindingGraph.ChildFactoryMethodEdge; 34 import dagger.model.BindingGraph.DependencyEdge; 35 import dagger.model.BindingGraph.Edge; 36 import dagger.model.BindingGraph.MaybeBinding; 37 import dagger.model.BindingGraph.MissingBinding; 38 import dagger.model.BindingGraph.Node; 39 import dagger.model.BindingGraph.SubcomponentCreatorBindingEdge; 40 import dagger.model.BindingKind; 41 import dagger.model.ComponentPath; 42 import dagger.spi.BindingGraphPlugin; 43 import dagger.spi.DiagnosticReporter; 44 import java.io.IOException; 45 import java.io.PrintWriter; 46 import java.util.ArrayList; 47 import java.util.HashMap; 48 import java.util.Iterator; 49 import java.util.LinkedHashMap; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.Optional; 53 import java.util.UUID; 54 import java.util.stream.Collectors; 55 import javax.annotation.processing.Filer; 56 import javax.lang.model.element.TypeElement; 57 import javax.tools.FileObject; 58 import javax.tools.StandardLocation; 59 60 /** 61 * Experimental visualizer used as a proof-of-concept for {@link BindingGraphPlugin}. 62 * 63 * <p>For each component, writes a <a href=http://www.graphviz.org/content/dot-language>DOT file</a> 64 * in the same package. The file name is the name of the component type (with enclosing type names, 65 * joined by underscores, preceding it), with a {@code .dot} extension. 66 * 67 * <p>For example, for a nested component type {@code Foo.Bar} this will generate a file {@code 68 * Foo_Bar.dot}. 69 */ 70 @AutoService(BindingGraphPlugin.class) 71 public final class BindingGraphVisualizer implements BindingGraphPlugin { 72 private Filer filer; 73 74 @Override initFiler(Filer filer)75 public void initFiler(Filer filer) { 76 this.filer = filer; 77 } 78 79 /** Graphviz color names to use for binding nodes within each component. */ 80 private static final ImmutableList<String> COMPONENT_COLORS = 81 ImmutableList.of( 82 "/set312/1", 83 "/set312/2", 84 "/set312/3", 85 "/set312/4", 86 "/set312/5", 87 "/set312/6", 88 "/set312/7", 89 "/set312/8", 90 "/set312/9", 91 "/set312/10", 92 "/set312/11", 93 "/set312/12"); 94 95 @Override visitGraph(BindingGraph bindingGraph, DiagnosticReporter diagnosticReporter)96 public void visitGraph(BindingGraph bindingGraph, DiagnosticReporter diagnosticReporter) { 97 TypeElement componentElement = 98 bindingGraph.rootComponentNode().componentPath().currentComponent(); 99 DotGraph graph = new NodesGraph(bindingGraph).graph(); 100 ClassName componentName = ClassName.get(componentElement); 101 try { 102 FileObject file = 103 filer 104 .createResource( 105 StandardLocation.CLASS_OUTPUT, 106 componentName.packageName(), 107 Joiner.on('_').join(componentName.simpleNames()) + ".dot", 108 componentElement); 109 try (PrintWriter writer = new PrintWriter(file.openWriter())) { 110 graph.write(0, writer); 111 } 112 } catch (IOException e) { 113 throw new RuntimeException(e); 114 } 115 } 116 117 private abstract static class Indented { 118 write(int level, PrintWriter writer)119 abstract void write(int level, PrintWriter writer); 120 121 @CanIgnoreReturnValue indent(int level, PrintWriter writer)122 PrintWriter indent(int level, PrintWriter writer) { 123 writer.print(Strings.repeat(" ", level * 2)); 124 return writer; 125 } 126 } 127 128 static class DotGraph extends Indented { 129 private final String header; 130 private final List<Indented> elements = new ArrayList<>(); 131 DotGraph(String header)132 DotGraph(String header) { 133 this.header = header; 134 } 135 136 @CanIgnoreReturnValue add(Indented element)137 DotGraph add(Indented element) { 138 elements.add(element); 139 return this; 140 } 141 142 @Override write(int level, PrintWriter writer)143 void write(int level, PrintWriter writer) { 144 indent(level, writer); 145 writer.println(header + " {"); 146 for (Indented element : elements) { 147 element.write(level + 1, writer); 148 } 149 indent(level, writer); 150 writer.println("}"); 151 } 152 } 153 154 static class DotStatement<S extends DotStatement<S>> extends Indented { 155 private final String base; 156 private final Map<String, Object> attributes = new LinkedHashMap<>(); 157 DotStatement(String base)158 DotStatement(String base) { 159 this.base = base; 160 } 161 162 @SuppressWarnings("unchecked") 163 @CanIgnoreReturnValue addAttribute(String name, Object value)164 S addAttribute(String name, Object value) { 165 attributes.put(name, value); 166 return (S) this; 167 } 168 169 @CanIgnoreReturnValue addAttributeFormat(String name, String format, Object... args)170 S addAttributeFormat(String name, String format, Object... args) { 171 return addAttribute(name, String.format(format, args)); 172 } 173 174 @Override write(int level, PrintWriter writer)175 void write(int level, PrintWriter writer) { 176 indent(level, writer); 177 writer.print(base); 178 if (!attributes.isEmpty()) { 179 writer.print( 180 attributes 181 .entrySet() 182 .stream() 183 .map( 184 entry -> 185 String.format("%s=%s", entry.getKey(), quote(entry.getValue().toString()))) 186 .collect(Collectors.joining(", ", " [", "]"))); 187 } 188 writer.println(); 189 } 190 } 191 quote(String string)192 private static String quote(String string) { 193 return '"' + string.replaceAll("\"", quoteReplacement("\\\"")) + '"'; 194 } 195 196 static class DotNode extends DotStatement<DotNode> { DotNode(Object nodeName)197 DotNode(Object nodeName) { 198 super(quote(nodeName.toString())); 199 } 200 } 201 202 static class DotEdge extends DotStatement<DotEdge> { DotEdge(Object leftNode, Object rightNode)203 DotEdge(Object leftNode, Object rightNode) { 204 super(quote(leftNode.toString()) + " -> " + quote(rightNode.toString())); 205 } 206 } 207 208 static class NodesGraph { 209 private final DotGraph graph = 210 new DotGraph("digraph") 211 .add( 212 new DotStatement<>("graph") 213 .addAttribute("rankdir", "LR") 214 .addAttribute("labeljust", "l") 215 .addAttribute("compound", true)); 216 217 private final BindingGraph bindingGraph; 218 private final Map<Node, UUID> nodeIds = new HashMap<>(); 219 NodesGraph(BindingGraph bindingGraph)220 NodesGraph(BindingGraph bindingGraph) { 221 this.bindingGraph = bindingGraph; 222 } 223 graph()224 DotGraph graph() { 225 if (nodeIds.isEmpty()) { 226 Iterator<String> colors = Iterators.cycle(COMPONENT_COLORS); 227 bindingGraph.network().nodes().stream() 228 .collect(groupingBy(Node::componentPath)) 229 .forEach( 230 (component, networkNodes) -> { 231 DotGraph subgraph = subgraph(component); 232 subgraph.add( 233 new DotStatement<>("node") 234 .addAttribute("style", "filled") 235 .addAttribute("shape", "box") 236 .addAttribute("fillcolor", colors.next())); 237 subgraph.add(new DotStatement<>("graph").addAttribute("label", component)); 238 for (Node node : networkNodes) { 239 subgraph.add(dotNode(node)); 240 } 241 }); 242 for (Edge edge : bindingGraph.network().edges()) { 243 dotEdge(edge).ifPresent(graph::add); 244 } 245 } 246 return graph; 247 } 248 subgraph(ComponentPath component)249 DotGraph subgraph(ComponentPath component) { 250 DotGraph subgraph = new DotGraph("subgraph " + quote(clusterName(component))); 251 graph.add(subgraph); 252 return subgraph; 253 } 254 nodeId(Node node)255 UUID nodeId(Node node) { 256 return nodeIds.computeIfAbsent(node, n -> randomUUID()); 257 } 258 dotEdge(Edge edge)259 Optional<DotEdge> dotEdge(Edge edge) { 260 EndpointPair<Node> incidentNodes = bindingGraph.network().incidentNodes(edge); 261 DotEdge dotEdge = new DotEdge(nodeId(incidentNodes.source()), nodeId(incidentNodes.target())); 262 if (edge instanceof DependencyEdge) { 263 if (((DependencyEdge) edge).isEntryPoint()) { 264 return Optional.empty(); 265 } 266 } else if (edge instanceof ChildFactoryMethodEdge) { 267 dotEdge.addAttribute("style", "dashed"); 268 dotEdge.addAttribute("lhead", clusterName(incidentNodes.target().componentPath())); 269 dotEdge.addAttribute("ltail", clusterName(incidentNodes.source().componentPath())); 270 dotEdge.addAttribute("taillabel", ((ChildFactoryMethodEdge) edge).factoryMethod()); 271 } else if (edge instanceof SubcomponentCreatorBindingEdge) { 272 dotEdge.addAttribute("style", "dashed"); 273 dotEdge.addAttribute("lhead", clusterName(incidentNodes.target().componentPath())); 274 dotEdge.addAttribute("taillabel", "subcomponent"); 275 } 276 return Optional.of(dotEdge); 277 } 278 dotNode(Node node)279 DotNode dotNode(Node node) { 280 DotNode dotNode = new DotNode(nodeId(node)); 281 if (node instanceof MaybeBinding) { 282 dotNode.addAttribute("tooltip", ""); 283 if (bindingGraph.entryPointBindings().contains(node)) { 284 dotNode.addAttribute("penwidth", 3); 285 } 286 if (node instanceof Binding) { 287 dotNode.addAttribute("label", label((Binding) node)); 288 } 289 if (node instanceof MissingBinding) { 290 dotNode.addAttributeFormat( 291 "label", "missing binding for %s", ((MissingBinding) node).key()); 292 } 293 } else { 294 dotNode.addAttribute("style", "invis").addAttribute("shape", "point"); 295 } 296 return dotNode; 297 } 298 label(Binding binding)299 private String label(Binding binding) { 300 if (binding.kind().equals(BindingKind.MEMBERS_INJECTION)) { 301 return String.format("inject(%s)", binding.key()); 302 } else if (binding.isProduction()) { 303 return String.format("@Produces %s", binding.key()); 304 } else { 305 return binding.key().toString(); 306 } 307 } 308 clusterName(ComponentPath owningComponentPath)309 private static String clusterName(ComponentPath owningComponentPath) { 310 return "cluster" + owningComponentPath; 311 } 312 } 313 } 314