• 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.validation;
18 
19 import static androidx.room.compiler.processing.XElementKt.isMethod;
20 import static androidx.room.compiler.processing.XElementKt.isMethodParameter;
21 import static com.google.common.base.Preconditions.checkArgument;
22 import static com.google.common.base.Preconditions.checkNotNull;
23 import static com.google.common.base.Predicates.in;
24 import static com.google.common.collect.Collections2.transform;
25 import static dagger.internal.codegen.base.ComponentAnnotation.rootComponentAnnotation;
26 import static dagger.internal.codegen.base.DiagnosticFormatting.stripCommonTypePrefixes;
27 import static dagger.internal.codegen.base.Formatter.INDENT;
28 import static dagger.internal.codegen.base.Scopes.getReadableSource;
29 import static dagger.internal.codegen.base.Util.reentrantComputeIfAbsent;
30 import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList;
31 import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet;
32 import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSetMultimap;
33 import static dagger.internal.codegen.xprocessing.XElements.asMethod;
34 import static dagger.internal.codegen.xprocessing.XElements.asMethodParameter;
35 import static dagger.internal.codegen.xprocessing.XElements.getSimpleName;
36 import static java.util.stream.Collectors.joining;
37 import static javax.tools.Diagnostic.Kind.ERROR;
38 
39 import androidx.room.compiler.processing.XAnnotation;
40 import androidx.room.compiler.processing.XElement;
41 import androidx.room.compiler.processing.XExecutableParameterElement;
42 import androidx.room.compiler.processing.XMethodElement;
43 import androidx.room.compiler.processing.XMethodType;
44 import androidx.room.compiler.processing.XType;
45 import androidx.room.compiler.processing.XTypeElement;
46 import com.google.common.collect.ImmutableSet;
47 import com.google.common.collect.ImmutableSetMultimap;
48 import com.google.common.collect.Multimaps;
49 import com.google.common.collect.Sets;
50 import com.squareup.javapoet.ClassName;
51 import com.squareup.javapoet.TypeName;
52 import dagger.internal.codegen.base.DaggerSuperficialValidation;
53 import dagger.internal.codegen.binding.ComponentCreatorDescriptor;
54 import dagger.internal.codegen.binding.ComponentDescriptor;
55 import dagger.internal.codegen.binding.ComponentRequirement;
56 import dagger.internal.codegen.binding.ComponentRequirement.NullPolicy;
57 import dagger.internal.codegen.binding.ContributionBinding;
58 import dagger.internal.codegen.binding.ErrorMessages;
59 import dagger.internal.codegen.binding.ErrorMessages.ComponentCreatorMessages;
60 import dagger.internal.codegen.binding.InjectionAnnotations;
61 import dagger.internal.codegen.binding.MethodSignatureFormatter;
62 import dagger.internal.codegen.binding.ModuleDescriptor;
63 import dagger.internal.codegen.compileroption.CompilerOptions;
64 import dagger.internal.codegen.compileroption.ValidationType;
65 import dagger.internal.codegen.javapoet.TypeNames;
66 import dagger.internal.codegen.model.Scope;
67 import dagger.internal.codegen.xprocessing.XTypes;
68 import java.util.ArrayDeque;
69 import java.util.Collection;
70 import java.util.Deque;
71 import java.util.LinkedHashMap;
72 import java.util.Map;
73 import java.util.Map.Entry;
74 import java.util.Optional;
75 import java.util.Set;
76 import java.util.StringJoiner;
77 import javax.inject.Inject;
78 import javax.tools.Diagnostic;
79 
80 /**
81  * Reports errors in the component hierarchy.
82  *
83  * <ul>
84  *   <li>Validates scope hierarchy of component dependencies and subcomponents.
85  *   <li>Reports errors if there are component dependency cycles.
86  *   <li>Reports errors if any abstract modules have non-abstract instance binding methods.
87  *   <li>Validates component creator types.
88  * </ul>
89  */
90 // TODO(dpb): Combine with ComponentHierarchyValidator.
91 public final class ComponentDescriptorValidator {
92 
93   private final CompilerOptions compilerOptions;
94   private final MethodSignatureFormatter methodSignatureFormatter;
95   private final ComponentHierarchyValidator componentHierarchyValidator;
96   private final InjectionAnnotations injectionAnnotations;
97   private final DaggerSuperficialValidation superficialValidation;
98 
99   @Inject
ComponentDescriptorValidator( CompilerOptions compilerOptions, MethodSignatureFormatter methodSignatureFormatter, ComponentHierarchyValidator componentHierarchyValidator, InjectionAnnotations injectionAnnotations, DaggerSuperficialValidation superficialValidation)100   ComponentDescriptorValidator(
101       CompilerOptions compilerOptions,
102       MethodSignatureFormatter methodSignatureFormatter,
103       ComponentHierarchyValidator componentHierarchyValidator,
104       InjectionAnnotations injectionAnnotations,
105       DaggerSuperficialValidation superficialValidation) {
106     this.compilerOptions = compilerOptions;
107     this.methodSignatureFormatter = methodSignatureFormatter;
108     this.componentHierarchyValidator = componentHierarchyValidator;
109     this.injectionAnnotations = injectionAnnotations;
110     this.superficialValidation = superficialValidation;
111   }
112 
validate(ComponentDescriptor component)113   public ValidationReport validate(ComponentDescriptor component) {
114     ComponentValidation validation = new ComponentValidation(component);
115     validation.visitComponent(component);
116     validation.report(component).addSubreport(componentHierarchyValidator.validate(component));
117     return validation.buildReport();
118   }
119 
120   private final class ComponentValidation {
121     final ComponentDescriptor rootComponent;
122     final Map<ComponentDescriptor, ValidationReport.Builder> reports = new LinkedHashMap<>();
123 
ComponentValidation(ComponentDescriptor rootComponent)124     ComponentValidation(ComponentDescriptor rootComponent) {
125       this.rootComponent = checkNotNull(rootComponent);
126     }
127 
128     /** Returns a report that contains all validation messages found during traversal. */
buildReport()129     ValidationReport buildReport() {
130       ValidationReport.Builder report = ValidationReport.about(rootComponent.typeElement());
131       reports.values().forEach(subreport -> report.addSubreport(subreport.build()));
132       return report.build();
133     }
134 
135     /** Returns the report builder for a (sub)component. */
report(ComponentDescriptor component)136     private ValidationReport.Builder report(ComponentDescriptor component) {
137       return reentrantComputeIfAbsent(
138           reports, component, descriptor -> ValidationReport.about(descriptor.typeElement()));
139     }
140 
reportComponentItem( Diagnostic.Kind kind, ComponentDescriptor component, String message)141     private void reportComponentItem(
142         Diagnostic.Kind kind, ComponentDescriptor component, String message) {
143       report(component)
144           .addItem(message, kind, component.typeElement(), component.annotation().annotation());
145     }
146 
reportComponentError(ComponentDescriptor component, String error)147     private void reportComponentError(ComponentDescriptor component, String error) {
148       reportComponentItem(ERROR, component, error);
149     }
150 
visitComponent(ComponentDescriptor component)151     void visitComponent(ComponentDescriptor component) {
152       validateDependencyScopes(component);
153       validateComponentDependencyHierarchy(component);
154       validateModules(component);
155       validateCreators(component);
156       component.childComponents().forEach(this::visitComponent);
157     }
158 
159     /** Validates that component dependencies do not form a cycle. */
validateComponentDependencyHierarchy(ComponentDescriptor component)160     private void validateComponentDependencyHierarchy(ComponentDescriptor component) {
161       validateComponentDependencyHierarchy(component, component.typeElement(), new ArrayDeque<>());
162     }
163 
164     /** Recursive method to validate that component dependencies do not form a cycle. */
validateComponentDependencyHierarchy( ComponentDescriptor component, XTypeElement dependency, Deque<XTypeElement> dependencyStack)165     private void validateComponentDependencyHierarchy(
166         ComponentDescriptor component,
167         XTypeElement dependency,
168         Deque<XTypeElement> dependencyStack) {
169       if (dependencyStack.contains(dependency)) {
170         // Current component has already appeared in the component chain.
171         StringBuilder message = new StringBuilder();
172         message.append(component.typeElement().getQualifiedName());
173         message.append(" contains a cycle in its component dependencies:\n");
174         dependencyStack.push(dependency);
175         appendIndentedComponentsList(message, dependencyStack);
176         dependencyStack.pop();
177         reportComponentItem(
178             compilerOptions.scopeCycleValidationType().diagnosticKind().get(),
179             component,
180             message.toString());
181       } else if (compilerOptions.validateTransitiveComponentDependencies()
182           // Always validate direct component dependencies referenced by this component regardless
183           // of the flag value
184           || dependencyStack.isEmpty()) {
185         rootComponentAnnotation(dependency, superficialValidation)
186             .ifPresent(
187                 componentAnnotation -> {
188                   dependencyStack.push(dependency);
189                   for (XTypeElement nextDependency : componentAnnotation.dependencies()) {
190                     validateComponentDependencyHierarchy(
191                         component, nextDependency, dependencyStack);
192                   }
193                   dependencyStack.pop();
194                 });
195       }
196     }
197 
198     /**
199      * Validates that among the dependencies there are no cycles within the scoping chain, and that
200      * singleton components have no scoped dependencies.
201      */
validateDependencyScopes(ComponentDescriptor component)202     private void validateDependencyScopes(ComponentDescriptor component) {
203       ImmutableSet<ClassName> scopes =
204           component.scopes().stream().map(Scope::className).collect(toImmutableSet());
205       ImmutableSet<XTypeElement> scopedDependencies =
206           scopedTypesIn(
207               component.dependencies().stream()
208                   .map(ComponentRequirement::typeElement)
209                   .collect(toImmutableSet()));
210       if (!scopes.isEmpty()) {
211         // Dagger 1.x scope compatibility requires this be suppress-able.
212         if (compilerOptions.scopeCycleValidationType().diagnosticKind().isPresent()
213             && (scopes.contains(TypeNames.SINGLETON)
214                 || scopes.contains(TypeNames.SINGLETON_JAVAX))) {
215           // Singleton is a special-case representing the longest lifetime, and therefore
216           // @Singleton components may not depend on scoped components
217           if (!scopedDependencies.isEmpty()) {
218             StringBuilder message =
219                 new StringBuilder(
220                     "This @Singleton component cannot depend on scoped components:\n");
221             appendIndentedComponentsList(message, scopedDependencies);
222             reportComponentItem(
223                 compilerOptions.scopeCycleValidationType().diagnosticKind().get(),
224                 component,
225                 message.toString());
226           }
227         } else {
228           // Dagger 1.x scope compatibility requires this be suppress-able.
229           if (!compilerOptions.scopeCycleValidationType().equals(ValidationType.NONE)) {
230             validateDependencyScopeHierarchy(
231                 component, component.typeElement(), new ArrayDeque<>(), new ArrayDeque<>());
232           }
233         }
234       } else {
235         // Scopeless components may not depend on scoped components.
236         if (!scopedDependencies.isEmpty()) {
237           StringBuilder message =
238               new StringBuilder(component.typeElement().getQualifiedName())
239                   .append(" (unscoped) cannot depend on scoped components:\n");
240           appendIndentedComponentsList(message, scopedDependencies);
241           reportComponentError(component, message.toString());
242         }
243       }
244     }
245 
validateModules(ComponentDescriptor component)246     private void validateModules(ComponentDescriptor component) {
247       for (ModuleDescriptor module : component.modules()) {
248         if (module.moduleElement().isAbstract()) {
249           for (ContributionBinding binding : module.bindings()) {
250             if (binding.requiresModuleInstance()) {
251               report(component).addError(abstractModuleHasInstanceBindingMethodsError(module));
252               break;
253             }
254           }
255         }
256       }
257     }
258 
abstractModuleHasInstanceBindingMethodsError(ModuleDescriptor module)259     private String abstractModuleHasInstanceBindingMethodsError(ModuleDescriptor module) {
260       String methodAnnotations;
261       switch (module.kind()) {
262         case MODULE:
263           methodAnnotations = "@Provides";
264           break;
265         case PRODUCER_MODULE:
266           methodAnnotations = "@Provides or @Produces";
267           break;
268         default:
269           throw new AssertionError(module.kind());
270       }
271       return String.format(
272           "%s is abstract and has instance %s methods. Consider making the methods static or "
273               + "including a non-abstract subclass of the module instead.",
274           module.moduleElement(), methodAnnotations);
275     }
276 
validateCreators(ComponentDescriptor component)277     private void validateCreators(ComponentDescriptor component) {
278       if (!component.creatorDescriptor().isPresent()) {
279         // If no builder, nothing to validate.
280         return;
281       }
282 
283       ComponentCreatorDescriptor creator = component.creatorDescriptor().get();
284       ComponentCreatorMessages messages = ErrorMessages.creatorMessagesFor(creator.annotation());
285 
286       // Requirements for modules and dependencies that the creator can set
287       Set<ComponentRequirement> creatorModuleAndDependencyRequirements =
288           creator.moduleAndDependencyRequirements();
289       // Modules and dependencies the component requires
290       Set<ComponentRequirement> componentModuleAndDependencyRequirements =
291           component.dependenciesAndConcreteModules();
292 
293       // Requirements that the creator can set that don't match any requirements that the component
294       // actually has.
295       Set<ComponentRequirement> inapplicableRequirementsOnCreator =
296           Sets.difference(
297               creatorModuleAndDependencyRequirements, componentModuleAndDependencyRequirements);
298 
299       XType container = creator.typeElement().getType();
300       if (!inapplicableRequirementsOnCreator.isEmpty()) {
301         Collection<XElement> excessElements =
302             Multimaps.filterKeys(
303                     creator.unvalidatedRequirementElements(), in(inapplicableRequirementsOnCreator))
304                 .values();
305         String formatted =
306             excessElements.stream()
307                 .map(element -> formatElement(element, container))
308                 .collect(joining(", ", "[", "]"));
309         report(component)
310             .addError(String.format(messages.extraSetters(), formatted), creator.typeElement());
311       }
312 
313       // Component requirements that the creator must be able to set
314       Set<ComponentRequirement> mustBePassed =
315           Sets.filter(
316               componentModuleAndDependencyRequirements,
317               input -> input.nullPolicy().equals(NullPolicy.THROW));
318       // Component requirements that the creator must be able to set, but can't
319       Set<ComponentRequirement> missingRequirements =
320           Sets.difference(mustBePassed, creatorModuleAndDependencyRequirements);
321 
322       if (!missingRequirements.isEmpty()) {
323         report(component)
324             .addError(
325                 String.format(
326                     messages.missingSetters(),
327                     missingRequirements.stream()
328                         .map(ComponentRequirement::type)
329                         .map(XTypes::toStableString)
330                         .collect(toImmutableList())),
331                 creator.typeElement());
332       }
333 
334       // Validate that declared creator requirements (modules, dependencies) have unique types.
335       ImmutableSetMultimap<TypeName, XElement> declaredRequirementsByType =
336           Multimaps.filterKeys(
337                   creator.unvalidatedRequirementElements(),
338                   creatorModuleAndDependencyRequirements::contains)
339               .entries()
340               .stream()
341               .collect(
342                   toImmutableSetMultimap(
343                       entry -> entry.getKey().type().getTypeName(), Entry::getValue));
344       declaredRequirementsByType
345           .asMap()
346           .forEach(
347               (type, elementsForType) -> {
348                 if (elementsForType.size() > 1) {
349                   // TODO(cgdecker): Attach this error message to the factory method rather than
350                   // the component type if the elements are factory method parameters AND the
351                   // factory method is defined by the factory type itself and not by a supertype.
352                   report(component)
353                       .addError(
354                           String.format(
355                               messages.multipleSettersForModuleOrDependencyType(),
356                               type,
357                               transform(
358                                   elementsForType, element -> formatElement(element, container))),
359                           creator.typeElement());
360                 }
361               });
362 
363       // TODO(cgdecker): Duplicate binding validation should handle the case of multiple elements
364       // that set the same bound-instance Key, but validating that here would make it fail faster
365       // for subcomponents.
366     }
367 
formatElement(XElement element, XType container)368     private String formatElement(XElement element, XType container) {
369       // TODO(cgdecker): Extract some or all of this to another class?
370       // But note that it does different formatting for parameters than
371       // DaggerElements.elementToString(Element).
372       if (isMethod(element)) {
373         return methodSignatureFormatter.format(asMethod(element), Optional.of(container));
374       } else if (isMethodParameter(element)) {
375         return formatParameter(asMethodParameter(element), container);
376       }
377       // This method shouldn't be called with any other type of element.
378       throw new AssertionError();
379     }
380 
formatParameter(XExecutableParameterElement parameter, XType container)381     private String formatParameter(XExecutableParameterElement parameter, XType container) {
382       // TODO(cgdecker): Possibly leave the type (and annotations?) off of the parameters here and
383       // just use their names, since the type will be redundant in the context of the error message.
384       StringJoiner joiner = new StringJoiner(" ");
385       parameter.getAllAnnotations().stream()
386           .map(XAnnotation::getQualifiedName)
387           .forEach(joiner::add);
388       XType parameterType = resolveParameterType(parameter, container);
389       return joiner
390           .add(stripCommonTypePrefixes(parameterType.getTypeName().toString()))
391           .add(getSimpleName(parameter))
392           .toString();
393     }
394 
resolveParameterType(XExecutableParameterElement parameter, XType container)395     private XType resolveParameterType(XExecutableParameterElement parameter, XType container) {
396       checkArgument(isMethod(parameter.getEnclosingElement()));
397       XMethodElement method = asMethod(parameter.getEnclosingElement());
398       int parameterIndex = method.getParameters().indexOf(parameter);
399 
400       XMethodType methodType = method.asMemberOf(container);
401       return methodType.getParameterTypes().get(parameterIndex);
402     }
403 
404     /**
405      * Validates that scopes do not participate in a scoping cycle - that is to say, scoped
406      * components are in a hierarchical relationship terminating with Singleton.
407      *
408      * <p>As a side-effect, this means scoped components cannot have a dependency cycle between
409      * themselves, since a component's presence within its own dependency path implies a cyclical
410      * relationship between scopes. However, cycles in component dependencies are explicitly checked
411      * in {@link #validateComponentDependencyHierarchy(ComponentDescriptor)}.
412      */
validateDependencyScopeHierarchy( ComponentDescriptor component, XTypeElement dependency, Deque<ImmutableSet<Scope>> scopeStack, Deque<XTypeElement> scopedDependencyStack)413     private void validateDependencyScopeHierarchy(
414         ComponentDescriptor component,
415         XTypeElement dependency,
416         Deque<ImmutableSet<Scope>> scopeStack,
417         Deque<XTypeElement> scopedDependencyStack) {
418       ImmutableSet<Scope> scopes = injectionAnnotations.getScopes(dependency);
419       if (stackOverlaps(scopeStack, scopes)) {
420         scopedDependencyStack.push(dependency);
421         // Current scope has already appeared in the component chain.
422         StringBuilder message = new StringBuilder();
423         message.append(component.typeElement().getQualifiedName());
424         message.append(" depends on scoped components in a non-hierarchical scope ordering:\n");
425         appendIndentedComponentsList(message, scopedDependencyStack);
426         if (compilerOptions.scopeCycleValidationType().diagnosticKind().isPresent()) {
427           reportComponentItem(
428               compilerOptions.scopeCycleValidationType().diagnosticKind().get(),
429               component,
430               message.toString());
431         }
432         scopedDependencyStack.pop();
433       } else if (compilerOptions.validateTransitiveComponentDependencies()
434           // Always validate direct component dependencies referenced by this component regardless
435           // of the flag value
436           || scopedDependencyStack.isEmpty()) {
437         // TODO(beder): transitively check scopes of production components too.
438         rootComponentAnnotation(dependency, superficialValidation)
439             .filter(componentAnnotation -> !componentAnnotation.isProduction())
440             .ifPresent(
441                 componentAnnotation -> {
442                   ImmutableSet<XTypeElement> scopedDependencies =
443                       scopedTypesIn(componentAnnotation.dependencies());
444                   if (!scopedDependencies.isEmpty()) {
445                     // empty can be ignored (base-case)
446                     scopeStack.push(scopes);
447                     scopedDependencyStack.push(dependency);
448                     for (XTypeElement scopedDependency : scopedDependencies) {
449                       validateDependencyScopeHierarchy(
450                           component, scopedDependency, scopeStack, scopedDependencyStack);
451                     }
452                     scopedDependencyStack.pop();
453                     scopeStack.pop();
454                   }
455                 }); // else: we skip component dependencies which are not components
456       }
457     }
458 
stackOverlaps(Deque<ImmutableSet<T>> stack, ImmutableSet<T> set)459     private <T> boolean stackOverlaps(Deque<ImmutableSet<T>> stack, ImmutableSet<T> set) {
460       for (ImmutableSet<T> entry : stack) {
461         if (!Sets.intersection(entry, set).isEmpty()) {
462           return true;
463         }
464       }
465       return false;
466     }
467 
468     /** Appends and formats a list of indented component types (with their scope annotations). */
appendIndentedComponentsList(StringBuilder message, Iterable<XTypeElement> types)469     private void appendIndentedComponentsList(StringBuilder message, Iterable<XTypeElement> types) {
470       for (XTypeElement scopedComponent : types) {
471         message.append(INDENT);
472         for (Scope scope : injectionAnnotations.getScopes(scopedComponent)) {
473           message.append(getReadableSource(scope)).append(' ');
474         }
475         message
476             .append(stripCommonTypePrefixes(scopedComponent.getQualifiedName().toString()))
477             .append('\n');
478       }
479     }
480 
481     /**
482      * Returns a set of type elements containing only those found in the input set that have a
483      * scoping annotation.
484      */
scopedTypesIn(Collection<XTypeElement> types)485     private ImmutableSet<XTypeElement> scopedTypesIn(Collection<XTypeElement> types) {
486       return types.stream()
487           .filter(type -> !injectionAnnotations.getScopes(type).isEmpty())
488           .collect(toImmutableSet());
489     }
490   }
491 }
492