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