/* * Copyright (C) 2017 The Dagger Authors. * * 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 dagger.example.spi; import static java.util.UUID.randomUUID; import static java.util.regex.Matcher.quoteReplacement; import static java.util.stream.Collectors.groupingBy; import com.google.auto.service.AutoService; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterators; import com.google.common.graph.EndpointPair; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.squareup.javapoet.ClassName; import dagger.model.Binding; import dagger.model.BindingGraph; import dagger.model.BindingGraph.ChildFactoryMethodEdge; import dagger.model.BindingGraph.DependencyEdge; import dagger.model.BindingGraph.Edge; import dagger.model.BindingGraph.MaybeBinding; import dagger.model.BindingGraph.MissingBinding; import dagger.model.BindingGraph.Node; import dagger.model.BindingGraph.SubcomponentCreatorBindingEdge; import dagger.model.BindingKind; import dagger.model.ComponentPath; import dagger.spi.BindingGraphPlugin; import dagger.spi.DiagnosticReporter; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; import javax.annotation.processing.Filer; import javax.lang.model.element.TypeElement; import javax.tools.FileObject; import javax.tools.StandardLocation; /** * Experimental visualizer used as a proof-of-concept for {@link BindingGraphPlugin}. * *

For each component, writes a DOT file * in the same package. The file name is the name of the component type (with enclosing type names, * joined by underscores, preceding it), with a {@code .dot} extension. * *

For example, for a nested component type {@code Foo.Bar} this will generate a file {@code * Foo_Bar.dot}. */ @AutoService(BindingGraphPlugin.class) public final class BindingGraphVisualizer implements BindingGraphPlugin { private Filer filer; @Override public void initFiler(Filer filer) { this.filer = filer; } /** Graphviz color names to use for binding nodes within each component. */ private static final ImmutableList COMPONENT_COLORS = ImmutableList.of( "/set312/1", "/set312/2", "/set312/3", "/set312/4", "/set312/5", "/set312/6", "/set312/7", "/set312/8", "/set312/9", "/set312/10", "/set312/11", "/set312/12"); @Override public void visitGraph(BindingGraph bindingGraph, DiagnosticReporter diagnosticReporter) { TypeElement componentElement = bindingGraph.rootComponentNode().componentPath().currentComponent(); DotGraph graph = new NodesGraph(bindingGraph).graph(); ClassName componentName = ClassName.get(componentElement); try { FileObject file = filer .createResource( StandardLocation.CLASS_OUTPUT, componentName.packageName(), Joiner.on('_').join(componentName.simpleNames()) + ".dot", componentElement); try (PrintWriter writer = new PrintWriter(file.openWriter())) { graph.write(0, writer); } } catch (IOException e) { throw new RuntimeException(e); } } private abstract static class Indented { abstract void write(int level, PrintWriter writer); @CanIgnoreReturnValue PrintWriter indent(int level, PrintWriter writer) { writer.print(Strings.repeat(" ", level * 2)); return writer; } } static class DotGraph extends Indented { private final String header; private final List elements = new ArrayList<>(); DotGraph(String header) { this.header = header; } @CanIgnoreReturnValue DotGraph add(Indented element) { elements.add(element); return this; } @Override void write(int level, PrintWriter writer) { indent(level, writer); writer.println(header + " {"); for (Indented element : elements) { element.write(level + 1, writer); } indent(level, writer); writer.println("}"); } } static class DotStatement> extends Indented { private final String base; private final Map attributes = new LinkedHashMap<>(); DotStatement(String base) { this.base = base; } @SuppressWarnings("unchecked") @CanIgnoreReturnValue S addAttribute(String name, Object value) { attributes.put(name, value); return (S) this; } @CanIgnoreReturnValue S addAttributeFormat(String name, String format, Object... args) { return addAttribute(name, String.format(format, args)); } @Override void write(int level, PrintWriter writer) { indent(level, writer); writer.print(base); if (!attributes.isEmpty()) { writer.print( attributes .entrySet() .stream() .map( entry -> String.format("%s=%s", entry.getKey(), quote(entry.getValue().toString()))) .collect(Collectors.joining(", ", " [", "]"))); } writer.println(); } } private static String quote(String string) { return '"' + string.replaceAll("\"", quoteReplacement("\\\"")) + '"'; } static class DotNode extends DotStatement { DotNode(Object nodeName) { super(quote(nodeName.toString())); } } static class DotEdge extends DotStatement { DotEdge(Object leftNode, Object rightNode) { super(quote(leftNode.toString()) + " -> " + quote(rightNode.toString())); } } static class NodesGraph { private final DotGraph graph = new DotGraph("digraph") .add( new DotStatement<>("graph") .addAttribute("rankdir", "LR") .addAttribute("labeljust", "l") .addAttribute("compound", true)); private final BindingGraph bindingGraph; private final Map nodeIds = new HashMap<>(); NodesGraph(BindingGraph bindingGraph) { this.bindingGraph = bindingGraph; } DotGraph graph() { if (nodeIds.isEmpty()) { Iterator colors = Iterators.cycle(COMPONENT_COLORS); bindingGraph.network().nodes().stream() .collect(groupingBy(Node::componentPath)) .forEach( (component, networkNodes) -> { DotGraph subgraph = subgraph(component); subgraph.add( new DotStatement<>("node") .addAttribute("style", "filled") .addAttribute("shape", "box") .addAttribute("fillcolor", colors.next())); subgraph.add(new DotStatement<>("graph").addAttribute("label", component)); for (Node node : networkNodes) { subgraph.add(dotNode(node)); } }); for (Edge edge : bindingGraph.network().edges()) { dotEdge(edge).ifPresent(graph::add); } } return graph; } DotGraph subgraph(ComponentPath component) { DotGraph subgraph = new DotGraph("subgraph " + quote(clusterName(component))); graph.add(subgraph); return subgraph; } UUID nodeId(Node node) { return nodeIds.computeIfAbsent(node, n -> randomUUID()); } Optional dotEdge(Edge edge) { EndpointPair incidentNodes = bindingGraph.network().incidentNodes(edge); DotEdge dotEdge = new DotEdge(nodeId(incidentNodes.source()), nodeId(incidentNodes.target())); if (edge instanceof DependencyEdge) { if (((DependencyEdge) edge).isEntryPoint()) { return Optional.empty(); } } else if (edge instanceof ChildFactoryMethodEdge) { dotEdge.addAttribute("style", "dashed"); dotEdge.addAttribute("lhead", clusterName(incidentNodes.target().componentPath())); dotEdge.addAttribute("ltail", clusterName(incidentNodes.source().componentPath())); dotEdge.addAttribute("taillabel", ((ChildFactoryMethodEdge) edge).factoryMethod()); } else if (edge instanceof SubcomponentCreatorBindingEdge) { dotEdge.addAttribute("style", "dashed"); dotEdge.addAttribute("lhead", clusterName(incidentNodes.target().componentPath())); dotEdge.addAttribute("taillabel", "subcomponent"); } return Optional.of(dotEdge); } DotNode dotNode(Node node) { DotNode dotNode = new DotNode(nodeId(node)); if (node instanceof MaybeBinding) { dotNode.addAttribute("tooltip", ""); if (bindingGraph.entryPointBindings().contains(node)) { dotNode.addAttribute("penwidth", 3); } if (node instanceof Binding) { dotNode.addAttribute("label", label((Binding) node)); } if (node instanceof MissingBinding) { dotNode.addAttributeFormat( "label", "missing binding for %s", ((MissingBinding) node).key()); } } else { dotNode.addAttribute("style", "invis").addAttribute("shape", "point"); } return dotNode; } private String label(Binding binding) { if (binding.kind().equals(BindingKind.MEMBERS_INJECTION)) { return String.format("inject(%s)", binding.key()); } else if (binding.isProduction()) { return String.format("@Produces %s", binding.key()); } else { return binding.key().toString(); } } private static String clusterName(ComponentPath owningComponentPath) { return "cluster" + owningComponentPath; } } }