• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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 com.google.common.base.Functions.constant;
20 import static com.google.common.base.Predicates.and;
21 import static com.google.common.base.Predicates.in;
22 import static com.google.common.base.Predicates.not;
23 import static dagger.internal.codegen.base.Scopes.getReadableSource;
24 import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet;
25 import static dagger.internal.codegen.xprocessing.XElements.getSimpleName;
26 
27 import androidx.room.compiler.processing.XExecutableParameterElement;
28 import androidx.room.compiler.processing.XTypeElement;
29 import com.google.common.base.Joiner;
30 import com.google.common.base.Predicate;
31 import com.google.common.collect.ImmutableMap;
32 import com.google.common.collect.ImmutableSet;
33 import com.google.common.collect.ImmutableSetMultimap;
34 import com.google.common.collect.Iterables;
35 import com.google.common.collect.LinkedHashMultimap;
36 import com.google.common.collect.Maps;
37 import com.google.common.collect.Multimaps;
38 import com.google.common.collect.SetMultimap;
39 import com.google.common.collect.Sets;
40 import dagger.internal.codegen.base.ModuleKind;
41 import dagger.internal.codegen.binding.ComponentDescriptor;
42 import dagger.internal.codegen.binding.ComponentDescriptor.ComponentMethodDescriptor;
43 import dagger.internal.codegen.binding.InjectionAnnotations;
44 import dagger.internal.codegen.binding.ModuleDescriptor;
45 import dagger.internal.codegen.compileroption.CompilerOptions;
46 import dagger.internal.codegen.model.Scope;
47 import java.util.Collection;
48 import java.util.Formatter;
49 import java.util.Map;
50 import java.util.Optional;
51 import javax.inject.Inject;
52 
53 /** Validates the relationships between parent components and subcomponents. */
54 final class ComponentHierarchyValidator {
55   private static final Joiner COMMA_SEPARATED_JOINER = Joiner.on(", ");
56 
57   private final CompilerOptions compilerOptions;
58   private final InjectionAnnotations injectionAnnotations;
59 
60   @Inject
ComponentHierarchyValidator( CompilerOptions compilerOptions, InjectionAnnotations injectionAnnotations)61   ComponentHierarchyValidator(
62       CompilerOptions compilerOptions, InjectionAnnotations injectionAnnotations) {
63     this.compilerOptions = compilerOptions;
64     this.injectionAnnotations = injectionAnnotations;
65   }
66 
validate(ComponentDescriptor componentDescriptor)67   ValidationReport validate(ComponentDescriptor componentDescriptor) {
68     ValidationReport.Builder report = ValidationReport.about(componentDescriptor.typeElement());
69     validateSubcomponentMethods(
70         report,
71         componentDescriptor,
72         Maps.toMap(componentDescriptor.moduleTypes(), constant(componentDescriptor.typeElement())));
73     validateRepeatedScopedDeclarations(report, componentDescriptor, LinkedHashMultimap.create());
74 
75     if (compilerOptions.scopeCycleValidationType().diagnosticKind().isPresent()) {
76       validateScopeHierarchy(
77           report, componentDescriptor, LinkedHashMultimap.<ComponentDescriptor, Scope>create());
78     }
79     validateProductionModuleUniqueness(report, componentDescriptor, LinkedHashMultimap.create());
80     return report.build();
81   }
82 
validateSubcomponentMethods( ValidationReport.Builder report, ComponentDescriptor componentDescriptor, ImmutableMap<XTypeElement, XTypeElement> existingModuleToOwners)83   private void validateSubcomponentMethods(
84       ValidationReport.Builder report,
85       ComponentDescriptor componentDescriptor,
86       ImmutableMap<XTypeElement, XTypeElement> existingModuleToOwners) {
87     componentDescriptor
88         .childComponentsDeclaredByFactoryMethods()
89         .forEach(
90             (method, childComponent) -> {
91               if (childComponent.hasCreator()) {
92                 report.addError(
93                     "Components may not have factory methods for subcomponents that define a "
94                         + "builder.",
95                     method.methodElement());
96               } else {
97                 validateFactoryMethodParameters(report, method, existingModuleToOwners);
98               }
99 
100               validateSubcomponentMethods(
101                   report,
102                   childComponent,
103                   new ImmutableMap.Builder<XTypeElement, XTypeElement>()
104                       .putAll(existingModuleToOwners)
105                       .putAll(
106                           Maps.toMap(
107                               Sets.difference(
108                                   childComponent.moduleTypes(), existingModuleToOwners.keySet()),
109                               constant(childComponent.typeElement())))
110                       .build());
111             });
112   }
113 
validateFactoryMethodParameters( ValidationReport.Builder report, ComponentMethodDescriptor subcomponentMethodDescriptor, ImmutableMap<XTypeElement, XTypeElement> existingModuleToOwners)114   private void validateFactoryMethodParameters(
115       ValidationReport.Builder report,
116       ComponentMethodDescriptor subcomponentMethodDescriptor,
117       ImmutableMap<XTypeElement, XTypeElement> existingModuleToOwners) {
118     for (XExecutableParameterElement factoryMethodParameter :
119         subcomponentMethodDescriptor.methodElement().getParameters()) {
120       XTypeElement moduleType = factoryMethodParameter.getType().getTypeElement();
121       if (existingModuleToOwners.containsKey(moduleType)) {
122         /* Factory method tries to pass a module that is already present in the parent.
123          * This is an error. */
124         report.addError(
125             String.format(
126                 "%s is present in %s. A subcomponent cannot use an instance of a "
127                     + "module that differs from its parent.",
128                 getSimpleName(moduleType),
129                 existingModuleToOwners.get(moduleType).getQualifiedName()),
130             factoryMethodParameter);
131       }
132     }
133   }
134 
135   /**
136    * Checks that components do not have any scopes that are also applied on any of their ancestors.
137    */
validateScopeHierarchy( ValidationReport.Builder report, ComponentDescriptor subject, SetMultimap<ComponentDescriptor, Scope> scopesByComponent)138   private void validateScopeHierarchy(
139       ValidationReport.Builder report,
140       ComponentDescriptor subject,
141       SetMultimap<ComponentDescriptor, Scope> scopesByComponent) {
142     scopesByComponent.putAll(subject, subject.scopes());
143 
144     for (ComponentDescriptor childComponent : subject.childComponents()) {
145       validateScopeHierarchy(report, childComponent, scopesByComponent);
146     }
147 
148     scopesByComponent.removeAll(subject);
149 
150     Predicate<Scope> subjectScopes =
151         subject.isProduction()
152             // TODO(beder): validate that @ProductionScope is only applied on production components
153             ? and(in(subject.scopes()), not(Scope::isProductionScope))
154             : in(subject.scopes());
155     SetMultimap<ComponentDescriptor, Scope> overlappingScopes =
156         Multimaps.filterValues(scopesByComponent, subjectScopes);
157     if (!overlappingScopes.isEmpty()) {
158       StringBuilder error =
159           new StringBuilder()
160               .append(subject.typeElement().getQualifiedName())
161               .append(" has conflicting scopes:");
162       for (Map.Entry<ComponentDescriptor, Scope> entry : overlappingScopes.entries()) {
163         Scope scope = entry.getValue();
164         error
165             .append("\n  ")
166             .append(entry.getKey().typeElement().getQualifiedName())
167             .append(" also has ")
168             .append(getReadableSource(scope));
169       }
170       report.addItem(
171           error.toString(),
172           compilerOptions.scopeCycleValidationType().diagnosticKind().get(),
173           subject.typeElement());
174     }
175   }
176 
validateProductionModuleUniqueness( ValidationReport.Builder report, ComponentDescriptor componentDescriptor, SetMultimap<ComponentDescriptor, ModuleDescriptor> producerModulesByComponent)177   private void validateProductionModuleUniqueness(
178       ValidationReport.Builder report,
179       ComponentDescriptor componentDescriptor,
180       SetMultimap<ComponentDescriptor, ModuleDescriptor> producerModulesByComponent) {
181     ImmutableSet<ModuleDescriptor> producerModules =
182         componentDescriptor.modules().stream()
183             .filter(module -> module.kind().equals(ModuleKind.PRODUCER_MODULE))
184             .collect(toImmutableSet());
185 
186     producerModulesByComponent.putAll(componentDescriptor, producerModules);
187     for (ComponentDescriptor childComponent : componentDescriptor.childComponents()) {
188       validateProductionModuleUniqueness(report, childComponent, producerModulesByComponent);
189     }
190     producerModulesByComponent.removeAll(componentDescriptor);
191 
192 
193     SetMultimap<ComponentDescriptor, ModuleDescriptor> repeatedModules =
194         Multimaps.filterValues(producerModulesByComponent, producerModules::contains);
195     if (repeatedModules.isEmpty()) {
196       return;
197     }
198 
199     StringBuilder error = new StringBuilder();
200     Formatter formatter = new Formatter(error);
201 
202     formatter.format(
203         "%s repeats @ProducerModules:", componentDescriptor.typeElement().getQualifiedName());
204 
205     for (Map.Entry<ComponentDescriptor, Collection<ModuleDescriptor>> entry :
206         repeatedModules.asMap().entrySet()) {
207       formatter.format("\n  %s also installs: ", entry.getKey().typeElement().getQualifiedName());
208       COMMA_SEPARATED_JOINER
209           .appendTo(
210               error,
211               Iterables.transform(entry.getValue(), m -> m.moduleElement().getQualifiedName()));
212     }
213 
214     report.addError(error.toString());
215   }
216 
validateRepeatedScopedDeclarations( ValidationReport.Builder report, ComponentDescriptor component, SetMultimap<ComponentDescriptor, ModuleDescriptor> modulesWithScopes)217   private void validateRepeatedScopedDeclarations(
218       ValidationReport.Builder report,
219       ComponentDescriptor component,
220       // TODO(ronshapiro): optimize ModuleDescriptor.hashCode()/equals. Otherwise this could be
221       // quite costly
222       SetMultimap<ComponentDescriptor, ModuleDescriptor> modulesWithScopes) {
223     ImmutableSet<ModuleDescriptor> modules =
224         component.modules().stream().filter(this::hasScopedDeclarations).collect(toImmutableSet());
225     modulesWithScopes.putAll(component, modules);
226     for (ComponentDescriptor childComponent : component.childComponents()) {
227       validateRepeatedScopedDeclarations(report, childComponent, modulesWithScopes);
228     }
229     modulesWithScopes.removeAll(component);
230 
231     SetMultimap<ComponentDescriptor, ModuleDescriptor> repeatedModules =
232         Multimaps.filterValues(modulesWithScopes, modules::contains);
233     if (repeatedModules.isEmpty()) {
234       return;
235     }
236 
237     report.addError(
238         repeatedModulesWithScopeError(component, ImmutableSetMultimap.copyOf(repeatedModules)));
239   }
240 
hasScopedDeclarations(ModuleDescriptor module)241   private boolean hasScopedDeclarations(ModuleDescriptor module) {
242     return !moduleScopes(module).isEmpty();
243   }
244 
repeatedModulesWithScopeError( ComponentDescriptor component, ImmutableSetMultimap<ComponentDescriptor, ModuleDescriptor> repeatedModules)245   private String repeatedModulesWithScopeError(
246       ComponentDescriptor component,
247       ImmutableSetMultimap<ComponentDescriptor, ModuleDescriptor> repeatedModules) {
248     StringBuilder error =
249         new StringBuilder()
250             .append(component.typeElement().getQualifiedName())
251             .append(" repeats modules with scoped bindings or declarations:");
252 
253     repeatedModules
254         .asMap()
255         .forEach(
256             (conflictingComponent, conflictingModules) -> {
257               error
258                   .append("\n  - ")
259                   .append(conflictingComponent.typeElement().getQualifiedName())
260                   .append(" also includes:");
261               for (ModuleDescriptor conflictingModule : conflictingModules) {
262                 error
263                     .append("\n    - ")
264                     .append(conflictingModule.moduleElement().getQualifiedName())
265                     .append(" with scopes: ")
266                     .append(COMMA_SEPARATED_JOINER.join(moduleScopes(conflictingModule)));
267               }
268             });
269     return error.toString();
270   }
271 
moduleScopes(ModuleDescriptor module)272   private ImmutableSet<Scope> moduleScopes(ModuleDescriptor module) {
273     return module.allBindingDeclarations().stream()
274         .map(declaration -> injectionAnnotations.getScope(declaration.bindingElement().get()))
275         .filter(scope -> scope.isPresent() && !scope.get().isReusable())
276         .map(Optional::get)
277         .collect(toImmutableSet());
278   }
279 }
280