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