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