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 androidx.room.compiler.processing.compat.XConverters.getProcessingEnv; 20 import static com.google.common.base.Verify.verify; 21 import static com.google.common.collect.Iterables.getLast; 22 import static com.google.common.collect.Iterables.getOnlyElement; 23 import static dagger.internal.codegen.base.ElementFormatter.elementToString; 24 import static dagger.internal.codegen.base.Formatter.INDENT; 25 import static dagger.internal.codegen.base.Keys.isValidImplicitProvisionKey; 26 import static dagger.internal.codegen.base.Keys.isValidMembersInjectionKey; 27 import static dagger.internal.codegen.base.RequestKinds.canBeSatisfiedByProductionBinding; 28 import static dagger.internal.codegen.binding.DependencyRequestFormatter.DOUBLE_INDENT; 29 import static dagger.internal.codegen.extension.DaggerStreams.instancesOf; 30 import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet; 31 import static dagger.internal.codegen.xprocessing.XTypes.isDeclared; 32 import static dagger.internal.codegen.xprocessing.XTypes.isWildcard; 33 import static javax.tools.Diagnostic.Kind.ERROR; 34 35 import androidx.room.compiler.processing.XType; 36 import com.google.common.collect.ImmutableList; 37 import com.google.common.collect.ImmutableSet; 38 import com.google.common.collect.Iterators; 39 import com.google.common.collect.Lists; 40 import com.squareup.javapoet.TypeName; 41 import com.squareup.javapoet.WildcardTypeName; 42 import dagger.internal.codegen.binding.ComponentNodeImpl; 43 import dagger.internal.codegen.binding.DependencyRequestFormatter; 44 import dagger.internal.codegen.binding.InjectBindingRegistry; 45 import dagger.internal.codegen.model.Binding; 46 import dagger.internal.codegen.model.BindingGraph; 47 import dagger.internal.codegen.model.BindingGraph.ComponentNode; 48 import dagger.internal.codegen.model.BindingGraph.DependencyEdge; 49 import dagger.internal.codegen.model.BindingGraph.Edge; 50 import dagger.internal.codegen.model.BindingGraph.MissingBinding; 51 import dagger.internal.codegen.model.BindingGraph.Node; 52 import dagger.internal.codegen.model.DaggerAnnotation; 53 import dagger.internal.codegen.model.DiagnosticReporter; 54 import dagger.internal.codegen.model.Key; 55 import dagger.internal.codegen.validation.DiagnosticMessageGenerator; 56 import dagger.internal.codegen.validation.ValidationBindingGraphPlugin; 57 import dagger.internal.codegen.xprocessing.XTypes; 58 import java.util.ArrayDeque; 59 import java.util.Deque; 60 import java.util.Iterator; 61 import java.util.List; 62 import java.util.Optional; 63 import javax.inject.Inject; 64 65 /** Reports errors for missing bindings. */ 66 final class MissingBindingValidator extends ValidationBindingGraphPlugin { 67 68 private final InjectBindingRegistry injectBindingRegistry; 69 private final DependencyRequestFormatter dependencyRequestFormatter; 70 private final DiagnosticMessageGenerator.Factory diagnosticMessageGeneratorFactory; 71 72 @Inject MissingBindingValidator( InjectBindingRegistry injectBindingRegistry, DependencyRequestFormatter dependencyRequestFormatter, DiagnosticMessageGenerator.Factory diagnosticMessageGeneratorFactory)73 MissingBindingValidator( 74 InjectBindingRegistry injectBindingRegistry, 75 DependencyRequestFormatter dependencyRequestFormatter, 76 DiagnosticMessageGenerator.Factory diagnosticMessageGeneratorFactory) { 77 this.injectBindingRegistry = injectBindingRegistry; 78 this.dependencyRequestFormatter = dependencyRequestFormatter; 79 this.diagnosticMessageGeneratorFactory = diagnosticMessageGeneratorFactory; 80 } 81 82 @Override pluginName()83 public String pluginName() { 84 return "Dagger/MissingBinding"; 85 } 86 87 @Override visitGraph(BindingGraph graph, DiagnosticReporter diagnosticReporter)88 public void visitGraph(BindingGraph graph, DiagnosticReporter diagnosticReporter) { 89 // Don't report missing bindings when validating a full binding graph or a graph built from a 90 // subcomponent. 91 if (graph.isFullBindingGraph() || graph.rootComponentNode().isSubcomponent()) { 92 return; 93 } 94 // A missing binding might exist in a different component as unused binding, thus getting 95 // stripped. Therefore, full graph needs to be traversed to capture the stripped bindings. 96 if (!graph.missingBindings().isEmpty()) { 97 requestVisitFullGraph(graph); 98 } 99 } 100 101 @Override revisitFullGraph( BindingGraph prunedGraph, BindingGraph fullGraph, DiagnosticReporter diagnosticReporter)102 public void revisitFullGraph( 103 BindingGraph prunedGraph, BindingGraph fullGraph, DiagnosticReporter diagnosticReporter) { 104 prunedGraph 105 .missingBindings() 106 .forEach( 107 missingBinding -> reportMissingBinding(missingBinding, fullGraph, diagnosticReporter)); 108 } 109 reportMissingBinding( MissingBinding missingBinding, BindingGraph graph, DiagnosticReporter diagnosticReporter)110 private void reportMissingBinding( 111 MissingBinding missingBinding, 112 BindingGraph graph, 113 DiagnosticReporter diagnosticReporter) { 114 diagnosticReporter.reportComponent( 115 ERROR, 116 graph.componentNode(missingBinding.componentPath()).get(), 117 missingBindingErrorMessage(missingBinding, graph) 118 + missingBindingDependencyTraceMessage(missingBinding, graph) 119 + alternativeBindingsMessage(missingBinding, graph) 120 + similarBindingsMessage(missingBinding, graph)); 121 } 122 getSimilarTypeBindings( BindingGraph graph, Key missingBindingKey)123 private static ImmutableSet<Binding> getSimilarTypeBindings( 124 BindingGraph graph, Key missingBindingKey) { 125 XType missingBindingType = missingBindingKey.type().xprocessing(); 126 Optional<DaggerAnnotation> missingBindingQualifier = missingBindingKey.qualifier(); 127 ImmutableList<TypeName> flatMissingBindingType = flattenBindingType(missingBindingType); 128 if (flatMissingBindingType.size() <= 1) { 129 return ImmutableSet.of(); 130 } 131 return graph.bindings().stream() 132 .filter( 133 binding -> 134 binding.key().qualifier().equals(missingBindingQualifier) 135 && isSimilarType(binding.key().type().xprocessing(), flatMissingBindingType)) 136 .collect(toImmutableSet()); 137 } 138 139 /** 140 * Unwraps a parameterized type to a list of TypeNames. e.g. {@code Map<Foo, List<Bar>>} to {@code 141 * [Map, Foo, List, Bar]}. 142 */ flattenBindingType(XType type)143 private static ImmutableList<TypeName> flattenBindingType(XType type) { 144 return ImmutableList.copyOf(new TypeDfsIterator(type)); 145 } 146 isSimilarType(XType type, List<TypeName> flatTypeNames)147 private static boolean isSimilarType(XType type, List<TypeName> flatTypeNames) { 148 return Iterators.elementsEqual(flatTypeNames.iterator(), new TypeDfsIterator(type)); 149 } 150 getBound(WildcardTypeName wildcardType)151 private static TypeName getBound(WildcardTypeName wildcardType) { 152 // Note: The javapoet API returns a list to be extensible, but there's currently no way to get 153 // multiple bounds, and it's not really clear what we should do if there were multiple bounds 154 // so we just assume there's only one for now. The javapoet API also guarantees that there will 155 // always be at least one upper bound -- in the absence of an explicit upper bound the Object 156 // type is used (e.g. Set<?> has an upper bound of Object). 157 return !wildcardType.lowerBounds.isEmpty() 158 ? getOnlyElement(wildcardType.lowerBounds) 159 : getOnlyElement(wildcardType.upperBounds); 160 } 161 missingBindingErrorMessage(MissingBinding missingBinding, BindingGraph graph)162 private String missingBindingErrorMessage(MissingBinding missingBinding, BindingGraph graph) { 163 Key key = missingBinding.key(); 164 StringBuilder errorMessage = new StringBuilder(); 165 // Wildcards should have already been checked by DependencyRequestValidator. 166 verify(!isWildcard(key.type().xprocessing()), "unexpected wildcard request: %s", key); 167 // TODO(ronshapiro): replace "provided" with "satisfied"? 168 errorMessage.append(key).append(" cannot be provided without "); 169 if (isValidImplicitProvisionKey(key)) { 170 errorMessage.append("an @Inject constructor or "); 171 } 172 errorMessage.append("an @Provides-"); // TODO(dpb): s/an/a 173 if (allIncomingDependenciesCanUseProduction(missingBinding, graph)) { 174 errorMessage.append(" or @Produces-"); 175 } 176 errorMessage.append("annotated method."); 177 if (isValidMembersInjectionKey(key) && typeHasInjectionSites(key)) { 178 errorMessage.append( 179 " This type supports members injection but cannot be implicitly provided."); 180 } 181 return errorMessage.toString(); 182 } 183 missingBindingDependencyTraceMessage( MissingBinding missingBinding, BindingGraph graph)184 private String missingBindingDependencyTraceMessage( 185 MissingBinding missingBinding, BindingGraph graph) { 186 ImmutableSet<DependencyEdge> entryPoints = 187 graph.entryPointEdgesDependingOnBinding(missingBinding); 188 DiagnosticMessageGenerator generator = diagnosticMessageGeneratorFactory.create(graph); 189 ImmutableList<DependencyEdge> dependencyTrace = 190 generator.dependencyTrace(missingBinding, entryPoints); 191 StringBuilder message = 192 new StringBuilder(dependencyTrace.size() * 100 /* a guess heuristic */).append("\n"); 193 for (DependencyEdge edge : dependencyTrace) { 194 String line = dependencyRequestFormatter.format(edge.dependencyRequest()); 195 if (line.isEmpty()) { 196 continue; 197 } 198 // We don't have to check for cases where component names collide since 199 // 1. We always show the full classname of the component, and 200 // 2. We always show the full component path at the end of the dependency trace (below). 201 String componentName = String.format("[%s] ", getComponentFromDependencyEdge(edge, graph)); 202 message.append("\n").append(line.replace(DOUBLE_INDENT, DOUBLE_INDENT + componentName)); 203 } 204 if (!dependencyTrace.isEmpty()) { 205 generator.appendComponentPathUnlessAtRoot(message, source(getLast(dependencyTrace), graph)); 206 } 207 message.append( 208 generator.getRequestsNotInTrace( 209 dependencyTrace, generator.requests(missingBinding), entryPoints)); 210 return message.toString(); 211 } 212 alternativeBindingsMessage( MissingBinding missingBinding, BindingGraph graph)213 private String alternativeBindingsMessage( 214 MissingBinding missingBinding, BindingGraph graph) { 215 ImmutableSet<Binding> alternativeBindings = graph.bindings(missingBinding.key()); 216 if (alternativeBindings.isEmpty()) { 217 return ""; 218 } 219 StringBuilder message = new StringBuilder(); 220 message.append("\n\nNote: ") 221 .append(missingBinding.key()) 222 .append(" is provided in the following other components:"); 223 for (Binding alternativeBinding : alternativeBindings) { 224 // Some alternative bindings appear multiple times because they were re-resolved in multiple 225 // components (e.g. due to multibinding contributions). To avoid the noise, we only report 226 // the binding where the module is contributed. 227 if (alternativeBinding.contributingModule().isPresent() 228 && !((ComponentNodeImpl) graph.componentNode(alternativeBinding.componentPath()).get()) 229 .componentDescriptor() 230 .moduleTypes() 231 .contains(alternativeBinding.contributingModule().get().xprocessing())) { 232 continue; 233 } 234 message.append("\n").append(INDENT).append(asString(alternativeBinding)); 235 } 236 return message.toString(); 237 } 238 similarBindingsMessage( MissingBinding missingBinding, BindingGraph graph)239 private String similarBindingsMessage( 240 MissingBinding missingBinding, BindingGraph graph) { 241 ImmutableSet<Binding> similarBindings = 242 getSimilarTypeBindings(graph, missingBinding.key()); 243 if (similarBindings.isEmpty()) { 244 return ""; 245 } 246 StringBuilder message = 247 new StringBuilder( 248 "\n\nNote: A similar binding is provided in the following other components:"); 249 for (Binding similarBinding : similarBindings) { 250 message 251 .append("\n") 252 .append(INDENT) 253 .append(similarBinding.key()) 254 .append(" is provided at:") 255 .append("\n") 256 .append(DOUBLE_INDENT) 257 .append(asString(similarBinding)); 258 } 259 message.append("\n") 260 .append( 261 "(For Kotlin sources, you may need to use '@JvmSuppressWildcards' or '@JvmWildcard' if " 262 + "you need to explicitly control the wildcards at a particular usage site.)"); 263 return message.toString(); 264 } 265 asString(Binding binding)266 private String asString(Binding binding) { 267 return String.format( 268 "[%s] %s", 269 binding.componentPath().currentComponent().xprocessing().getQualifiedName(), 270 binding.bindingElement().isPresent() 271 ? elementToString( 272 binding.bindingElement().get().xprocessing(), 273 /* elideMethodParameterTypes= */ true) 274 // For synthetic bindings just print the Binding#toString() 275 : binding); 276 } 277 allIncomingDependenciesCanUseProduction( MissingBinding missingBinding, BindingGraph graph)278 private boolean allIncomingDependenciesCanUseProduction( 279 MissingBinding missingBinding, BindingGraph graph) { 280 return graph.network().inEdges(missingBinding).stream() 281 .flatMap(instancesOf(DependencyEdge.class)) 282 .allMatch(edge -> dependencyCanBeProduction(edge, graph)); 283 } 284 285 // TODO(ronshapiro): merge with 286 // ProvisionDependencyOnProduerBindingValidator.dependencyCanUseProduction dependencyCanBeProduction(DependencyEdge edge, BindingGraph graph)287 private boolean dependencyCanBeProduction(DependencyEdge edge, BindingGraph graph) { 288 Node source = graph.network().incidentNodes(edge).source(); 289 if (source instanceof ComponentNode) { 290 return canBeSatisfiedByProductionBinding(edge.dependencyRequest().kind()); 291 } 292 if (source instanceof Binding) { 293 return ((Binding) source).isProduction(); 294 } 295 throw new IllegalArgumentException( 296 "expected a dagger.internal.codegen.model.Binding or ComponentNode: " + source); 297 } 298 typeHasInjectionSites(Key key)299 private boolean typeHasInjectionSites(Key key) { 300 return injectBindingRegistry 301 .getOrFindMembersInjectionBinding(key) 302 .map(binding -> !binding.injectionSites().isEmpty()) 303 .orElse(false); 304 } 305 getComponentFromDependencyEdge(DependencyEdge edge, BindingGraph graph)306 private static String getComponentFromDependencyEdge(DependencyEdge edge, BindingGraph graph) { 307 return source(edge, graph).componentPath().currentComponent().className().canonicalName(); 308 } 309 source(Edge edge, BindingGraph graph)310 private static Node source(Edge edge, BindingGraph graph) { 311 return graph.network().incidentNodes(edge).source(); 312 } 313 314 /** 315 * An iterator over a list of TypeNames produced by flattening a parameterized type. e.g. {@code 316 * Map<Foo, List<Bar>>} to {@code [Map, Foo, List, Bar]}. 317 * 318 * <p>The iterator returns the bound when encounters a wildcard type. 319 */ 320 private static class TypeDfsIterator implements Iterator<TypeName> { 321 final Deque<XType> stack = new ArrayDeque<>(); 322 TypeDfsIterator(XType root)323 TypeDfsIterator(XType root) { 324 stack.push(root); 325 } 326 327 @Override hasNext()328 public boolean hasNext() { 329 return !stack.isEmpty(); 330 } 331 332 @Override next()333 public TypeName next() { 334 XType next = stack.pop(); 335 if (isDeclared(next)) { 336 if (XTypes.isRawParameterizedType(next)) { 337 XType obj = getProcessingEnv(next).requireType(TypeName.OBJECT); 338 for (int i = 0; i < next.getTypeElement().getType().getTypeArguments().size(); i++) { 339 stack.push(obj); 340 } 341 } else { 342 for (XType arg : Lists.reverse(next.getTypeArguments())) { 343 stack.push(arg); 344 } 345 } 346 } 347 return getBaseTypeName(next); 348 } 349 getBaseTypeName(XType type)350 private static TypeName getBaseTypeName(XType type) { 351 if (isDeclared(type)) { 352 return type.getRawType().getTypeName(); 353 } 354 TypeName typeName = type.getTypeName(); 355 if (typeName instanceof WildcardTypeName) { 356 return getBound((WildcardTypeName) typeName); 357 } 358 return typeName; 359 } 360 } 361 } 362