• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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.hilt.processor.internal.aggregateddeps;
18 
19 import static com.google.common.base.Preconditions.checkNotNull;
20 import static com.google.common.base.Preconditions.checkState;
21 import static com.google.common.collect.Iterables.getOnlyElement;
22 import static dagger.hilt.processor.internal.aggregateddeps.AggregatedDepsGenerator.AGGREGATING_PACKAGE;
23 import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList;
24 import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet;
25 
26 import com.google.auto.value.AutoValue;
27 import com.google.common.collect.HashMultimap;
28 import com.google.common.collect.ImmutableList;
29 import com.google.common.collect.ImmutableMap;
30 import com.google.common.collect.ImmutableSet;
31 import com.google.common.collect.ImmutableSetMultimap;
32 import com.google.common.collect.SetMultimap;
33 import com.squareup.javapoet.ClassName;
34 import dagger.hilt.processor.internal.AnnotationValues;
35 import dagger.hilt.processor.internal.BadInputException;
36 import dagger.hilt.processor.internal.ClassNames;
37 import dagger.hilt.processor.internal.ComponentDescriptor;
38 import dagger.hilt.processor.internal.ProcessorErrors;
39 import dagger.hilt.processor.internal.Processors;
40 import dagger.hilt.processor.internal.aggregateddeps.ComponentDependencies.AggregatedDepMetadata.DependencyType;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.Optional;
44 import javax.lang.model.element.AnnotationMirror;
45 import javax.lang.model.element.AnnotationValue;
46 import javax.lang.model.element.Element;
47 import javax.lang.model.element.ElementKind;
48 import javax.lang.model.element.PackageElement;
49 import javax.lang.model.element.TypeElement;
50 import javax.lang.model.util.Elements;
51 
52 /** Represents information needed to create a component (i.e. modules, entry points, etc) */
53 @AutoValue
54 public abstract class ComponentDependencies {
builder()55   private static Builder builder() {
56     return new AutoValue_ComponentDependencies.Builder();
57   }
58 
59   /** Returns the modules for a component, without any filtering. */
modules()60   public abstract Dependencies modules();
61 
62   /** Returns the entry points associated with the given a component. */
entryPoints()63   public abstract Dependencies entryPoints();
64 
65   /** Returns the component entry point associated with the given a component. */
componentEntryPoints()66   public abstract Dependencies componentEntryPoints();
67 
68   @AutoValue.Builder
69   abstract static class Builder {
modulesBuilder()70     abstract Dependencies.Builder modulesBuilder();
71 
entryPointsBuilder()72     abstract Dependencies.Builder entryPointsBuilder();
73 
componentEntryPointsBuilder()74     abstract Dependencies.Builder componentEntryPointsBuilder();
75 
autoBuild()76     abstract ComponentDependencies autoBuild();
77 
build(Elements elements)78     ComponentDependencies build(Elements elements) {
79       validateModules(modulesBuilder().build(), elements);
80       return autoBuild();
81     }
82   }
83 
84   /** A key used for grouping a test dependency by both its component and test name. */
85   @AutoValue
86   abstract static class TestDepKey {
of(ClassName component, ClassName test)87     static TestDepKey of(ClassName component, ClassName test) {
88       return new AutoValue_ComponentDependencies_TestDepKey(component, test);
89     }
90 
91     /** Returns the name of the component this dependency should be installed in. */
component()92     abstract ClassName component();
93 
94     /** Returns the name of the test that this dependency should be installed in. */
test()95     abstract ClassName test();
96   }
97 
98   /**
99    * Holds a set of component dependencies, e.g. modules or entry points.
100    *
101    * <p>This class handles separating dependencies into global and test dependencies. Global
102    * dependencies are installed with every test, where test dependencies are only installed with the
103    * specified test. The total set of dependencies includes all global + test dependencies.
104    */
105   @AutoValue
106   public abstract static class Dependencies {
builder()107     static Builder builder() {
108       return new AutoValue_ComponentDependencies_Dependencies.Builder();
109     }
110 
111     /** Returns the global deps keyed by component. */
globalDeps()112     abstract ImmutableSetMultimap<ClassName, TypeElement> globalDeps();
113 
114     /** Returns the global test deps keyed by component. */
globalTestDeps()115     abstract ImmutableSetMultimap<ClassName, TypeElement> globalTestDeps();
116 
117     /** Returns the test deps keyed by component and test. */
testDeps()118     abstract ImmutableSetMultimap<TestDepKey, TypeElement> testDeps();
119 
120     /** Returns the uninstalled test deps keyed by test. */
uninstalledTestDeps()121     abstract ImmutableSetMultimap<ClassName, TypeElement> uninstalledTestDeps();
122 
123     /** Returns the global uninstalled test deps. */
globalUninstalledTestDeps()124     abstract ImmutableSet<TypeElement> globalUninstalledTestDeps();
125 
126     /** Returns the dependencies to be installed in the given component for the given root. */
get(ClassName component, ClassName root, boolean isTestRoot)127     public ImmutableSet<TypeElement> get(ClassName component, ClassName root, boolean isTestRoot) {
128       if (!isTestRoot) {
129         return globalDeps().get(component);
130       }
131 
132       ImmutableSet<TypeElement> uninstalledTestDepsForRoot = uninstalledTestDeps().get(root);
133       return ImmutableSet.<TypeElement>builder()
134           .addAll(
135               globalDeps().get(component).stream()
136                   .filter(dep -> !uninstalledTestDepsForRoot.contains(dep))
137                   .filter(dep -> !globalUninstalledTestDeps().contains(dep))
138                   .collect(toImmutableSet()))
139           .addAll(globalTestDeps().get(component))
140           .addAll(testDeps().get(TestDepKey.of(component, root)))
141           .build();
142     }
143 
144     @AutoValue.Builder
145     abstract static class Builder {
globalDepsBuilder()146       abstract ImmutableSetMultimap.Builder<ClassName, TypeElement> globalDepsBuilder();
147 
globalTestDepsBuilder()148       abstract ImmutableSetMultimap.Builder<ClassName, TypeElement> globalTestDepsBuilder();
149 
testDepsBuilder()150       abstract ImmutableSetMultimap.Builder<TestDepKey, TypeElement> testDepsBuilder();
151 
uninstalledTestDepsBuilder()152       abstract ImmutableSetMultimap.Builder<ClassName, TypeElement> uninstalledTestDepsBuilder();
153 
globalUninstalledTestDepsBuilder()154       abstract ImmutableSet.Builder<TypeElement> globalUninstalledTestDepsBuilder();
155 
build()156       abstract Dependencies build();
157     }
158   }
159 
160   /**
161    * Pulls the component dependencies from the {@code packageName}.
162    *
163    * <p>Dependency files are generated by the {@link AggregatedDepsProcessor}, and have the form:
164    *
165    * <pre>{@code
166    * {@literal @}AggregatedDeps(
167    *   components = {
168    *       "foo.FooComponent",
169    *       "bar.BarComponent"
170    *   },
171    *   modules = "baz.BazModule"
172    * )
173    *
174    * }</pre>
175    */
from( ImmutableSet<ComponentDescriptor> descriptors, Elements elements)176   public static ComponentDependencies from(
177       ImmutableSet<ComponentDescriptor> descriptors, Elements elements) {
178     Map<String, ComponentDescriptor> descriptorLookup = descriptorLookupMap(descriptors);
179     ImmutableList<AggregatedDepMetadata> metadatas =
180         getAggregatedDeps(elements).stream()
181             .map(deps -> AggregatedDepMetadata.create(deps, descriptorLookup, elements))
182             .collect(toImmutableList());
183 
184     ComponentDependencies.Builder componentDependencies = ComponentDependencies.builder();
185     for (AggregatedDepMetadata metadata : metadatas) {
186       Dependencies.Builder builder = null;
187       switch (metadata.dependencyType()) {
188         case MODULE:
189           builder = componentDependencies.modulesBuilder();
190           break;
191         case ENTRY_POINT:
192           builder = componentDependencies.entryPointsBuilder();
193           break;
194         case COMPONENT_ENTRY_POINT:
195           builder = componentDependencies.componentEntryPointsBuilder();
196           break;
197       }
198 
199       for (ComponentDescriptor componentDescriptor : metadata.componentDescriptors()) {
200         ClassName component = componentDescriptor.component();
201         if (metadata.testElement().isPresent()) {
202           // In this case the @InstallIn or @TestInstallIn applies to only the given test root.
203           ClassName test = ClassName.get(metadata.testElement().get());
204           builder.testDepsBuilder().put(TestDepKey.of(component, test), metadata.dependency());
205           builder.uninstalledTestDepsBuilder().putAll(test, metadata.replacedDependencies());
206         } else {
207           // In this case the @InstallIn or @TestInstallIn applies to all roots
208           if (!metadata.replacedDependencies().isEmpty()) {
209             // If there are replacedDependencies() it means this is a @TestInstallIn
210             builder.globalTestDepsBuilder().put(component, metadata.dependency());
211             builder.globalUninstalledTestDepsBuilder().addAll(metadata.replacedDependencies());
212           } else {
213             builder.globalDepsBuilder().put(component, metadata.dependency());
214           }
215         }
216       }
217     }
218 
219     // Collect all @UninstallModules.
220     // TODO(b/176438516): Filter @UninstallModules at the root.
221     metadatas.stream()
222         .filter(metadata -> metadata.testElement().isPresent())
223         .map(metadata -> metadata.testElement().get())
224         .distinct()
225         .filter(testElement -> Processors.hasAnnotation(testElement, ClassNames.IGNORE_MODULES))
226         .forEach(
227             testElement ->
228                 componentDependencies
229                     .modulesBuilder()
230                     .uninstalledTestDepsBuilder()
231                     .putAll(
232                         ClassName.get(testElement), getUninstalledModules(testElement, elements)));
233 
234     return componentDependencies.build(elements);
235   }
236 
descriptorLookupMap( ImmutableSet<ComponentDescriptor> descriptors)237   private static ImmutableMap<String, ComponentDescriptor> descriptorLookupMap(
238       ImmutableSet<ComponentDescriptor> descriptors) {
239     ImmutableMap.Builder<String, ComponentDescriptor> builder = ImmutableMap.builder();
240     for (ComponentDescriptor descriptor : descriptors) {
241       // This is a temporary hack to map the old ApplicationComponent to the new SingletonComponent.
242       // Technically, this is only needed for backwards compatibility with libraries using the old
243       // processor since new processors should convert to the new SingletonComponent when generating
244       // the metadata class.
245       if (descriptor.component().equals(ClassNames.SINGLETON_COMPONENT)) {
246         builder.put("dagger.hilt.android.components.ApplicationComponent", descriptor);
247       }
248       builder.put(descriptor.component().toString(), descriptor);
249     }
250     return builder.build();
251   }
252 
253   // Validate that the @UninstallModules doesn't contain any test modules.
validateModules(Dependencies moduleDeps, Elements elements)254   private static Dependencies validateModules(Dependencies moduleDeps, Elements elements) {
255     SetMultimap<ClassName, TypeElement> invalidTestModules = HashMultimap.create();
256     moduleDeps.testDeps().entries().stream()
257         .filter(
258             e -> moduleDeps.uninstalledTestDeps().containsEntry(e.getKey().test(), e.getValue()))
259         .forEach(e -> invalidTestModules.put(e.getKey().test(), e.getValue()));
260 
261     // Currently we don't have a good way to throw an error for all tests, so we sort (to keep the
262     // error reporting order stable) and then choose the first test.
263     // TODO(bcorso): Consider using ProcessorErrorHandler directly to report all errors at once?
264     Optional<ClassName> invalidTest =
265         invalidTestModules.keySet().stream()
266             .min((test1, test2) -> test1.toString().compareTo(test2.toString()));
267     if (invalidTest.isPresent()) {
268       throw new BadInputException(
269           String.format(
270               "@UninstallModules on test, %s, should not containing test modules, "
271                   + "but found: %s",
272               invalidTest.get(),
273               invalidTestModules.get(invalidTest.get()).stream()
274                   // Sort modules to keep stable error messages.
275                   .sorted((test1, test2) -> test1.toString().compareTo(test2.toString()))
276                   .collect(toImmutableList())),
277           elements.getTypeElement(invalidTest.get().toString()));
278     }
279     return moduleDeps;
280   }
281 
getUninstalledModules( TypeElement testElement, Elements elements)282   private static ImmutableSet<TypeElement> getUninstalledModules(
283       TypeElement testElement, Elements elements) {
284     ImmutableList<TypeElement> userUninstallModules =
285         Processors.getAnnotationClassValues(
286             elements,
287             Processors.getAnnotationMirror(testElement, ClassNames.IGNORE_MODULES),
288             "value");
289 
290     // For pkg-private modules, find the generated wrapper class and uninstall that instead.
291     return userUninstallModules.stream()
292         .map(uninstallModule -> getPublicDependency(uninstallModule, elements))
293         .collect(toImmutableSet());
294   }
295 
296   /** Returns the public Hilt wrapper module, or the module itself if its already public. */
getPublicDependency(TypeElement dependency, Elements elements)297   private static TypeElement getPublicDependency(TypeElement dependency, Elements elements) {
298     return PkgPrivateMetadata.of(elements, dependency, ClassNames.MODULE)
299         .map(metadata -> elements.getTypeElement(metadata.generatedClassName().toString()))
300         .orElse(dependency);
301   }
302 
303   /** Returns the top-level elements of the aggregated deps package. */
getAggregatedDeps(Elements elements)304   private static ImmutableList<AnnotationMirror> getAggregatedDeps(Elements elements) {
305     PackageElement packageElement = elements.getPackageElement(AGGREGATING_PACKAGE);
306     checkState(
307         packageElement != null,
308         "Couldn't find package %s. Did you mark your @Module classes with @InstallIn annotations?",
309         AGGREGATING_PACKAGE);
310 
311     List<? extends Element> aggregatedDepsElements = packageElement.getEnclosedElements();
312     checkState(
313         !aggregatedDepsElements.isEmpty(),
314         "No dependencies found. Did you mark your @Module classes with @InstallIn annotations?");
315 
316     ImmutableList.Builder<AnnotationMirror> builder = ImmutableList.builder();
317     for (Element element : aggregatedDepsElements) {
318       ProcessorErrors.checkState(
319           element.getKind() == ElementKind.CLASS,
320           element,
321           "Only classes may be in package %s. Did you add custom code in the package?",
322           AGGREGATING_PACKAGE);
323 
324       AnnotationMirror aggregatedDeps =
325           Processors.getAnnotationMirror(element, ClassNames.AGGREGATED_DEPS);
326       ProcessorErrors.checkState(
327           aggregatedDeps != null,
328           element,
329           "Classes in package %s must be annotated with @AggregatedDeps: %s. Found: %s.",
330           AGGREGATING_PACKAGE,
331           element.getSimpleName(),
332           element.getAnnotationMirrors());
333 
334       builder.add(aggregatedDeps);
335     }
336     return builder.build();
337   }
338 
339   @AutoValue
340   abstract static class AggregatedDepMetadata {
create( AnnotationMirror aggregatedDeps, Map<String, ComponentDescriptor> descriptorLookup, Elements elements)341     static AggregatedDepMetadata create(
342         AnnotationMirror aggregatedDeps,
343         Map<String, ComponentDescriptor> descriptorLookup,
344         Elements elements) {
345       ImmutableMap<String, AnnotationValue> aggregatedDepsValues =
346           Processors.getAnnotationValues(elements, aggregatedDeps);
347 
348       return new AutoValue_ComponentDependencies_AggregatedDepMetadata(
349           getTestElement(aggregatedDepsValues.get("test"), elements),
350           getComponents(aggregatedDepsValues.get("components"), descriptorLookup),
351           getDependencyType(
352               aggregatedDepsValues.get("modules"),
353               aggregatedDepsValues.get("entryPoints"),
354               aggregatedDepsValues.get("componentEntryPoints")),
355           getDependency(
356               aggregatedDepsValues.get("modules"),
357               aggregatedDepsValues.get("entryPoints"),
358               aggregatedDepsValues.get("componentEntryPoints"),
359               elements),
360           getReplacedDependencies(aggregatedDepsValues.get("replaces"), elements));
361     }
362 
363     enum DependencyType {
364       MODULE,
365       ENTRY_POINT,
366       COMPONENT_ENTRY_POINT
367     }
368 
testElement()369     abstract Optional<TypeElement> testElement();
370 
componentDescriptors()371     abstract ImmutableList<ComponentDescriptor> componentDescriptors();
372 
dependencyType()373     abstract DependencyType dependencyType();
374 
dependency()375     abstract TypeElement dependency();
376 
replacedDependencies()377     abstract ImmutableSet<TypeElement> replacedDependencies();
378 
getTestElement( AnnotationValue testValue, Elements elements)379     private static Optional<TypeElement> getTestElement(
380         AnnotationValue testValue, Elements elements) {
381       checkNotNull(testValue);
382       String test = AnnotationValues.getString(testValue);
383       return test.isEmpty() ? Optional.empty() : Optional.of(elements.getTypeElement(test));
384     }
385 
getComponents( AnnotationValue componentsValue, Map<String, ComponentDescriptor> descriptorLookup)386     private static ImmutableList<ComponentDescriptor> getComponents(
387         AnnotationValue componentsValue, Map<String, ComponentDescriptor> descriptorLookup) {
388       checkNotNull(componentsValue);
389       ImmutableList<String> componentNames =
390           AnnotationValues.getAnnotationValues(componentsValue).stream()
391               .map(AnnotationValues::getString)
392               .collect(toImmutableList());
393 
394       checkState(!componentNames.isEmpty());
395       ImmutableList.Builder<ComponentDescriptor> components = ImmutableList.builder();
396       for (String componentName : componentNames) {
397         checkState(
398             descriptorLookup.containsKey(componentName),
399             "%s is not a valid Component. Did you add or remove code in package %s?",
400             componentName,
401             AGGREGATING_PACKAGE);
402         components.add(descriptorLookup.get(componentName));
403       }
404       return components.build();
405     }
406 
getDependencyType( AnnotationValue modulesValue, AnnotationValue entryPointsValue, AnnotationValue componentEntryPointsValue)407     private static DependencyType getDependencyType(
408         AnnotationValue modulesValue,
409         AnnotationValue entryPointsValue,
410         AnnotationValue componentEntryPointsValue) {
411       checkNotNull(modulesValue);
412       checkNotNull(entryPointsValue);
413       checkNotNull(componentEntryPointsValue);
414 
415       ImmutableSet.Builder<DependencyType> dependencyTypes = ImmutableSet.builder();
416       if (!AnnotationValues.getAnnotationValues(modulesValue).isEmpty()) {
417         dependencyTypes.add(DependencyType.MODULE);
418       }
419       if (!AnnotationValues.getAnnotationValues(entryPointsValue).isEmpty()) {
420         dependencyTypes.add(DependencyType.ENTRY_POINT);
421       }
422       if (!AnnotationValues.getAnnotationValues(componentEntryPointsValue).isEmpty()) {
423         dependencyTypes.add(DependencyType.COMPONENT_ENTRY_POINT);
424       }
425       return getOnlyElement(dependencyTypes.build());
426     }
427 
getDependency( AnnotationValue modulesValue, AnnotationValue entryPointsValue, AnnotationValue componentEntryPointsValue, Elements elements)428     private static TypeElement getDependency(
429         AnnotationValue modulesValue,
430         AnnotationValue entryPointsValue,
431         AnnotationValue componentEntryPointsValue,
432         Elements elements) {
433       checkNotNull(modulesValue);
434       checkNotNull(entryPointsValue);
435       checkNotNull(componentEntryPointsValue);
436 
437       return elements.getTypeElement(
438           AnnotationValues.getString(
439               getOnlyElement(
440                   ImmutableList.<AnnotationValue>builder()
441                       .addAll(AnnotationValues.getAnnotationValues(modulesValue))
442                       .addAll(AnnotationValues.getAnnotationValues(entryPointsValue))
443                       .addAll(AnnotationValues.getAnnotationValues(componentEntryPointsValue))
444                       .build())));
445     }
446 
getReplacedDependencies( AnnotationValue replacedDependenciesValue, Elements elements)447     private static ImmutableSet<TypeElement> getReplacedDependencies(
448         AnnotationValue replacedDependenciesValue, Elements elements) {
449       // Allow null values to support libraries using a Hilt version before @TestInstallIn was added
450       return replacedDependenciesValue == null
451           ? ImmutableSet.of()
452           : AnnotationValues.getAnnotationValues(replacedDependenciesValue).stream()
453               .map(AnnotationValues::getString)
454               .map(elements::getTypeElement)
455               .map(replacedDep -> getPublicDependency(replacedDep, elements))
456               .collect(toImmutableSet());
457     }
458   }
459 }
460