• 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 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