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