• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.internal.codegen.bindinggraphvalidation;
18 
19 import static com.google.common.base.Preconditions.checkArgument;
20 import static com.google.common.collect.Iterables.getLast;
21 import static com.google.common.collect.Iterables.limit;
22 import static com.google.common.collect.Iterables.skip;
23 import static com.google.common.collect.Sets.newHashSetWithExpectedSize;
24 import static dagger.internal.codegen.base.RequestKinds.extractKeyType;
25 import static dagger.internal.codegen.base.RequestKinds.getRequestKind;
26 import static dagger.internal.codegen.extension.DaggerGraphs.shortestPath;
27 import static dagger.internal.codegen.extension.DaggerStreams.instancesOf;
28 import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList;
29 import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet;
30 import static javax.tools.Diagnostic.Kind.ERROR;
31 
32 import androidx.room.compiler.processing.XType;
33 import com.google.auto.value.AutoValue;
34 import com.google.common.collect.ImmutableList;
35 import com.google.common.collect.ImmutableSet;
36 import com.google.common.collect.Iterables;
37 import com.google.common.graph.EndpointPair;
38 import com.google.common.graph.Graphs;
39 import com.google.common.graph.ImmutableNetwork;
40 import com.google.common.graph.MutableNetwork;
41 import com.google.common.graph.NetworkBuilder;
42 import dagger.internal.codegen.base.Formatter;
43 import dagger.internal.codegen.base.MapType;
44 import dagger.internal.codegen.base.OptionalType;
45 import dagger.internal.codegen.binding.DependencyRequestFormatter;
46 import dagger.internal.codegen.javapoet.TypeNames;
47 import dagger.internal.codegen.model.Binding;
48 import dagger.internal.codegen.model.BindingGraph;
49 import dagger.internal.codegen.model.BindingGraph.ComponentNode;
50 import dagger.internal.codegen.model.BindingGraph.DependencyEdge;
51 import dagger.internal.codegen.model.BindingGraph.Node;
52 import dagger.internal.codegen.model.BindingKind;
53 import dagger.internal.codegen.model.DependencyRequest;
54 import dagger.internal.codegen.model.DiagnosticReporter;
55 import dagger.internal.codegen.model.RequestKind;
56 import dagger.internal.codegen.validation.ValidationBindingGraphPlugin;
57 import java.util.List;
58 import java.util.Optional;
59 import java.util.Set;
60 import java.util.stream.Stream;
61 import javax.inject.Inject;
62 
63 /** Reports errors for dependency cycles. */
64 final class DependencyCycleValidator extends ValidationBindingGraphPlugin {
65 
66   private final DependencyRequestFormatter dependencyRequestFormatter;
67 
68   @Inject
DependencyCycleValidator(DependencyRequestFormatter dependencyRequestFormatter)69   DependencyCycleValidator(DependencyRequestFormatter dependencyRequestFormatter) {
70     this.dependencyRequestFormatter = dependencyRequestFormatter;
71   }
72 
73   @Override
pluginName()74   public String pluginName() {
75     return "Dagger/DependencyCycle";
76   }
77 
78   @Override
visitGraph(BindingGraph bindingGraph, DiagnosticReporter diagnosticReporter)79   public void visitGraph(BindingGraph bindingGraph, DiagnosticReporter diagnosticReporter) {
80     ImmutableNetwork<Node, DependencyEdge> dependencyGraph =
81         nonCycleBreakingDependencyGraph(bindingGraph);
82     // First check the graph for a cycle. If there is one, then we'll do more work to report where.
83     if (!Graphs.hasCycle(dependencyGraph)) {
84       return;
85     }
86     // Check each endpoint pair only once, no matter how many parallel edges connect them.
87     Set<EndpointPair<Node>> dependencyEndpointPairs = dependencyGraph.asGraph().edges();
88     Set<EndpointPair<Node>> visited = newHashSetWithExpectedSize(dependencyEndpointPairs.size());
89     for (EndpointPair<Node> endpointPair : dependencyEndpointPairs) {
90       cycleContainingEndpointPair(endpointPair, dependencyGraph, visited)
91           .ifPresent(cycle -> reportCycle(cycle, bindingGraph, diagnosticReporter));
92     }
93   }
94 
cycleContainingEndpointPair( EndpointPair<Node> endpoints, ImmutableNetwork<Node, DependencyEdge> dependencyGraph, Set<EndpointPair<Node>> visited)95   private Optional<Cycle<Node>> cycleContainingEndpointPair(
96       EndpointPair<Node> endpoints,
97       ImmutableNetwork<Node, DependencyEdge> dependencyGraph,
98       Set<EndpointPair<Node>> visited) {
99     if (!visited.add(endpoints)) {
100       // don't recheck endpoints we already know are part of a cycle
101       return Optional.empty();
102     }
103 
104     // If there's a path from the target back to the source, there's a cycle.
105     ImmutableList<Node> cycleNodes =
106         shortestPath(dependencyGraph, endpoints.target(), endpoints.source());
107     if (cycleNodes.isEmpty()) {
108       return Optional.empty();
109     }
110 
111     Cycle<Node> cycle = Cycle.fromPath(cycleNodes);
112     visited.addAll(cycle.endpointPairs()); // no need to check any edge in this cycle again
113     return Optional.of(cycle);
114   }
115 
116   /**
117    * Reports a dependency cycle at the dependency into the cycle that is closest to an entry point.
118    *
119    * <p>For cycles found in reachable binding graphs, looks for the shortest path from the component
120    * that contains the cycle (all bindings in a cycle must be in the same component; see below) to
121    * some binding in the cycle. Then looks for the last dependency in that path that is not in the
122    * cycle; that is the dependency that will be reported, so that the dependency trace will end just
123    * before the cycle.
124    *
125    * <p>For cycles found during full binding graph validation, just reports the component that
126    * contains the cycle.
127    *
128    * <p>Proof (by counterexample) that all bindings in a cycle must be in the same component: Assume
129    * one binding in the cycle is in a parent component. Bindings cannot depend on bindings in child
130    * components, so that binding cannot depend on the next binding in the cycle.
131    */
reportCycle( Cycle<Node> cycle, BindingGraph bindingGraph, DiagnosticReporter diagnosticReporter)132   private void reportCycle(
133       Cycle<Node> cycle, BindingGraph bindingGraph, DiagnosticReporter diagnosticReporter) {
134     if (bindingGraph.isFullBindingGraph()) {
135       diagnosticReporter.reportComponent(
136           ERROR,
137           bindingGraph.componentNode(cycle.nodes().asList().get(0).componentPath()).get(),
138           errorMessage(cycle, bindingGraph));
139       return;
140     }
141 
142     ImmutableList<Node> path = shortestPathToCycleFromAnEntryPoint(cycle, bindingGraph);
143     Node cycleStartNode = path.get(path.size() - 1);
144     Node previousNode = path.get(path.size() - 2);
145     DependencyEdge dependencyToReport =
146         chooseDependencyEdgeConnecting(previousNode, cycleStartNode, bindingGraph);
147     diagnosticReporter.reportDependency(
148         ERROR,
149         dependencyToReport,
150         errorMessage(cycle.shift(cycleStartNode), bindingGraph)
151         // The actual dependency trace is included from the reportDependency call.
152             + "\n\nThe cycle is requested via:");
153   }
154 
shortestPathToCycleFromAnEntryPoint( Cycle<Node> cycle, BindingGraph bindingGraph)155   private ImmutableList<Node> shortestPathToCycleFromAnEntryPoint(
156       Cycle<Node> cycle, BindingGraph bindingGraph) {
157     Node someCycleNode = cycle.nodes().asList().get(0);
158     ComponentNode componentContainingCycle =
159         bindingGraph.componentNode(someCycleNode.componentPath()).get();
160     ImmutableList<Node> pathToCycle =
161         shortestPath(bindingGraph.network(), componentContainingCycle, someCycleNode);
162     return subpathToCycle(pathToCycle, cycle);
163   }
164 
165   /**
166    * Returns the subpath from the head of {@code path} to the first node in {@code path} that's in
167    * the cycle.
168    */
subpathToCycle(ImmutableList<Node> path, Cycle<Node> cycle)169   private ImmutableList<Node> subpathToCycle(ImmutableList<Node> path, Cycle<Node> cycle) {
170     ImmutableList.Builder<Node> subpath = ImmutableList.builder();
171     for (Node node : path) {
172       subpath.add(node);
173       if (cycle.nodes().contains(node)) {
174         return subpath.build();
175       }
176     }
177     throw new IllegalArgumentException(
178         "path " + path + " doesn't contain any nodes in cycle " + cycle);
179   }
180 
errorMessage(Cycle<Node> cycle, BindingGraph graph)181   private String errorMessage(Cycle<Node> cycle, BindingGraph graph) {
182     StringBuilder message = new StringBuilder("Found a dependency cycle:");
183     ImmutableList<DependencyRequest> cycleRequests =
184         cycle.endpointPairs().stream()
185             // TODO(dpb): Would be nice to take the dependency graph here.
186             .map(endpointPair -> nonCycleBreakingEdge(endpointPair, graph))
187             .map(DependencyEdge::dependencyRequest)
188             .collect(toImmutableList())
189             .reverse();
190     dependencyRequestFormatter.formatIndentedList(message, cycleRequests, 0);
191     message.append("\n")
192         .append(dependencyRequestFormatter.format(cycleRequests.get(0)))
193         .append("\n")
194         .append(Formatter.INDENT).append("...");
195     return message.toString();
196   }
197 
198   /**
199    * Returns one of the edges between two nodes that doesn't {@linkplain
200    * #breaksCycle(DependencyEdge, BindingGraph) break} a cycle.
201    */
nonCycleBreakingEdge(EndpointPair<Node> endpointPair, BindingGraph graph)202   private DependencyEdge nonCycleBreakingEdge(EndpointPair<Node> endpointPair, BindingGraph graph) {
203     return graph.network().edgesConnecting(endpointPair.source(), endpointPair.target()).stream()
204         .flatMap(instancesOf(DependencyEdge.class))
205         .filter(edge -> !breaksCycle(edge, graph))
206         .findFirst()
207         .get();
208   }
209 
breaksCycle(DependencyEdge edge, BindingGraph graph)210   private boolean breaksCycle(DependencyEdge edge, BindingGraph graph) {
211     // Map<K, V> multibindings depend on Map<K, Provider<V>> entries, but those don't break any
212     // cycles, so ignore them.
213     if (edge.dependencyRequest().key().multibindingContributionIdentifier().isPresent()) {
214       return false;
215     }
216     if (breaksCycle(
217         edge.dependencyRequest().key().type().xprocessing(), edge.dependencyRequest().kind())) {
218       return true;
219     }
220     Node target = graph.network().incidentNodes(edge).target();
221     if (target instanceof Binding && ((Binding) target).kind().equals(BindingKind.OPTIONAL)) {
222       /* For @BindsOptionalOf bindings, unwrap the type inside the Optional. If the unwrapped type
223        * breaks the cycle, so does the optional binding. */
224       XType optionalValueType = OptionalType.from(edge.dependencyRequest().key()).valueType();
225       RequestKind requestKind = getRequestKind(optionalValueType);
226       return breaksCycle(extractKeyType(optionalValueType), requestKind);
227     }
228     return false;
229   }
230 
breaksCycle(XType requestedType, RequestKind requestKind)231   private boolean breaksCycle(XType requestedType, RequestKind requestKind) {
232     switch (requestKind) {
233       case PROVIDER:
234       case LAZY:
235       case PROVIDER_OF_LAZY:
236         return true;
237 
238       case INSTANCE:
239         if (MapType.isMap(requestedType)) {
240           return MapType.from(requestedType).valuesAreTypeOf(TypeNames.PROVIDER);
241         }
242         // fall through
243 
244       default:
245         return false;
246     }
247   }
248 
chooseDependencyEdgeConnecting( Node source, Node target, BindingGraph bindingGraph)249   private DependencyEdge chooseDependencyEdgeConnecting(
250       Node source, Node target, BindingGraph bindingGraph) {
251     return bindingGraph.network().edgesConnecting(source, target).stream()
252         .flatMap(instancesOf(DependencyEdge.class))
253         .findFirst()
254         .get();
255   }
256 
257   /** Returns the subgraph containing only {@link DependencyEdge}s that would not break a cycle. */
258   // TODO(dpb): Return a network containing only Binding nodes.
nonCycleBreakingDependencyGraph( BindingGraph bindingGraph)259   private ImmutableNetwork<Node, DependencyEdge> nonCycleBreakingDependencyGraph(
260       BindingGraph bindingGraph) {
261     MutableNetwork<Node, DependencyEdge> dependencyNetwork =
262         NetworkBuilder.from(bindingGraph.network())
263             .expectedNodeCount(bindingGraph.network().nodes().size())
264             .expectedEdgeCount(bindingGraph.dependencyEdges().size())
265             .build();
266     bindingGraph.dependencyEdges().stream()
267         .filter(edge -> !breaksCycle(edge, bindingGraph))
268         .forEach(
269             edge -> {
270               EndpointPair<Node> endpoints = bindingGraph.network().incidentNodes(edge);
271               dependencyNetwork.addEdge(endpoints.source(), endpoints.target(), edge);
272             });
273     return ImmutableNetwork.copyOf(dependencyNetwork);
274   }
275 
276   /**
277    * An ordered set of endpoint pairs representing the edges in the cycle. The target of each pair
278    * is the source of the next pair. The target of the last pair is the source of the first pair.
279    */
280   @AutoValue
281   abstract static class Cycle<N> {
282     /**
283      * The ordered set of endpoint pairs representing the edges in the cycle. The target of each
284      * pair is the source of the next pair. The target of the last pair is the source of the first
285      * pair.
286      */
endpointPairs()287     abstract ImmutableSet<EndpointPair<N>> endpointPairs();
288 
289     /** Returns the nodes that participate in the cycle. */
nodes()290     ImmutableSet<N> nodes() {
291       return endpointPairs().stream()
292           .flatMap(pair -> Stream.of(pair.source(), pair.target()))
293           .collect(toImmutableSet());
294     }
295 
296     /** Returns the number of edges in the cycle. */
size()297     int size() {
298       return endpointPairs().size();
299     }
300 
301     /**
302      * Shifts this cycle so that it starts with a specific node.
303      *
304      * @return a cycle equivalent to this one but whose first pair starts with {@code startNode}
305      */
shift(N startNode)306     Cycle<N> shift(N startNode) {
307       int startIndex = Iterables.indexOf(endpointPairs(), pair -> pair.source().equals(startNode));
308       checkArgument(
309           startIndex >= 0, "startNode (%s) is not part of this cycle: %s", startNode, this);
310       if (startIndex == 0) {
311         return this;
312       }
313       ImmutableSet.Builder<EndpointPair<N>> shifted = ImmutableSet.builder();
314       shifted.addAll(skip(endpointPairs(), startIndex));
315       shifted.addAll(limit(endpointPairs(), size() - startIndex));
316       return new AutoValue_DependencyCycleValidator_Cycle<>(shifted.build());
317     }
318 
319     @Override
toString()320     public final String toString() {
321       return endpointPairs().toString();
322     }
323 
324     /**
325      * Creates a {@link Cycle} from a nonempty list of nodes, assuming there is an edge between each
326      * pair of nodes as well as an edge from the last node to the first.
327      */
fromPath(List<N> nodes)328     static <N> Cycle<N> fromPath(List<N> nodes) {
329       checkArgument(!nodes.isEmpty());
330       ImmutableSet.Builder<EndpointPair<N>> cycle = ImmutableSet.builder();
331       cycle.add(EndpointPair.ordered(getLast(nodes), nodes.get(0)));
332       for (int i = 0; i < nodes.size() - 1; i++) {
333         cycle.add(EndpointPair.ordered(nodes.get(i), nodes.get(i + 1)));
334       }
335       return new AutoValue_DependencyCycleValidator_Cycle<>(cycle.build());
336     }
337   }
338 }
339