/* * Copyright (C) 2019 The Dagger Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package dagger.hilt.processor.internal.aggregateddeps; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.Iterables.getOnlyElement; import static dagger.hilt.processor.internal.aggregateddeps.AggregatedDepsGenerator.AGGREGATING_PACKAGE; import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList; import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet; import com.google.auto.value.AutoValue; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.SetMultimap; import com.squareup.javapoet.ClassName; import dagger.hilt.processor.internal.AnnotationValues; import dagger.hilt.processor.internal.BadInputException; import dagger.hilt.processor.internal.ClassNames; import dagger.hilt.processor.internal.ComponentDescriptor; import dagger.hilt.processor.internal.ProcessorErrors; import dagger.hilt.processor.internal.Processors; import dagger.hilt.processor.internal.aggregateddeps.ComponentDependencies.AggregatedDepMetadata.DependencyType; import java.util.List; import java.util.Map; import java.util.Optional; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import javax.lang.model.util.Elements; /** Represents information needed to create a component (i.e. modules, entry points, etc) */ @AutoValue public abstract class ComponentDependencies { private static Builder builder() { return new AutoValue_ComponentDependencies.Builder(); } /** Returns the modules for a component, without any filtering. */ public abstract Dependencies modules(); /** Returns the entry points associated with the given a component. */ public abstract Dependencies entryPoints(); /** Returns the component entry point associated with the given a component. */ public abstract Dependencies componentEntryPoints(); @AutoValue.Builder abstract static class Builder { abstract Dependencies.Builder modulesBuilder(); abstract Dependencies.Builder entryPointsBuilder(); abstract Dependencies.Builder componentEntryPointsBuilder(); abstract ComponentDependencies autoBuild(); ComponentDependencies build(Elements elements) { validateModules(modulesBuilder().build(), elements); return autoBuild(); } } /** A key used for grouping a test dependency by both its component and test name. */ @AutoValue abstract static class TestDepKey { static TestDepKey of(ClassName component, ClassName test) { return new AutoValue_ComponentDependencies_TestDepKey(component, test); } /** Returns the name of the component this dependency should be installed in. */ abstract ClassName component(); /** Returns the name of the test that this dependency should be installed in. */ abstract ClassName test(); } /** * Holds a set of component dependencies, e.g. modules or entry points. * *

This class handles separating dependencies into global and test dependencies. Global * dependencies are installed with every test, where test dependencies are only installed with the * specified test. The total set of dependencies includes all global + test dependencies. */ @AutoValue public abstract static class Dependencies { static Builder builder() { return new AutoValue_ComponentDependencies_Dependencies.Builder(); } /** Returns the global deps keyed by component. */ abstract ImmutableSetMultimap globalDeps(); /** Returns the global test deps keyed by component. */ abstract ImmutableSetMultimap globalTestDeps(); /** Returns the test deps keyed by component and test. */ abstract ImmutableSetMultimap testDeps(); /** Returns the uninstalled test deps keyed by test. */ abstract ImmutableSetMultimap uninstalledTestDeps(); /** Returns the global uninstalled test deps. */ abstract ImmutableSet globalUninstalledTestDeps(); /** Returns the dependencies to be installed in the given component for the given root. */ public ImmutableSet get(ClassName component, ClassName root, boolean isTestRoot) { if (!isTestRoot) { return globalDeps().get(component); } ImmutableSet uninstalledTestDepsForRoot = uninstalledTestDeps().get(root); return ImmutableSet.builder() .addAll( globalDeps().get(component).stream() .filter(dep -> !uninstalledTestDepsForRoot.contains(dep)) .filter(dep -> !globalUninstalledTestDeps().contains(dep)) .collect(toImmutableSet())) .addAll(globalTestDeps().get(component)) .addAll(testDeps().get(TestDepKey.of(component, root))) .build(); } @AutoValue.Builder abstract static class Builder { abstract ImmutableSetMultimap.Builder globalDepsBuilder(); abstract ImmutableSetMultimap.Builder globalTestDepsBuilder(); abstract ImmutableSetMultimap.Builder testDepsBuilder(); abstract ImmutableSetMultimap.Builder uninstalledTestDepsBuilder(); abstract ImmutableSet.Builder globalUninstalledTestDepsBuilder(); abstract Dependencies build(); } } /** * Pulls the component dependencies from the {@code packageName}. * *

Dependency files are generated by the {@link AggregatedDepsProcessor}, and have the form: * *

{@code
   * {@literal @}AggregatedDeps(
   *   components = {
   *       "foo.FooComponent",
   *       "bar.BarComponent"
   *   },
   *   modules = "baz.BazModule"
   * )
   *
   * }
*/ public static ComponentDependencies from( ImmutableSet descriptors, Elements elements) { Map descriptorLookup = descriptorLookupMap(descriptors); ImmutableList metadatas = getAggregatedDeps(elements).stream() .map(deps -> AggregatedDepMetadata.create(deps, descriptorLookup, elements)) .collect(toImmutableList()); ComponentDependencies.Builder componentDependencies = ComponentDependencies.builder(); for (AggregatedDepMetadata metadata : metadatas) { Dependencies.Builder builder = null; switch (metadata.dependencyType()) { case MODULE: builder = componentDependencies.modulesBuilder(); break; case ENTRY_POINT: builder = componentDependencies.entryPointsBuilder(); break; case COMPONENT_ENTRY_POINT: builder = componentDependencies.componentEntryPointsBuilder(); break; } for (ComponentDescriptor componentDescriptor : metadata.componentDescriptors()) { ClassName component = componentDescriptor.component(); if (metadata.testElement().isPresent()) { // In this case the @InstallIn or @TestInstallIn applies to only the given test root. ClassName test = ClassName.get(metadata.testElement().get()); builder.testDepsBuilder().put(TestDepKey.of(component, test), metadata.dependency()); builder.uninstalledTestDepsBuilder().putAll(test, metadata.replacedDependencies()); } else { // In this case the @InstallIn or @TestInstallIn applies to all roots if (!metadata.replacedDependencies().isEmpty()) { // If there are replacedDependencies() it means this is a @TestInstallIn builder.globalTestDepsBuilder().put(component, metadata.dependency()); builder.globalUninstalledTestDepsBuilder().addAll(metadata.replacedDependencies()); } else { builder.globalDepsBuilder().put(component, metadata.dependency()); } } } } // Collect all @UninstallModules. // TODO(b/176438516): Filter @UninstallModules at the root. metadatas.stream() .filter(metadata -> metadata.testElement().isPresent()) .map(metadata -> metadata.testElement().get()) .distinct() .filter(testElement -> Processors.hasAnnotation(testElement, ClassNames.IGNORE_MODULES)) .forEach( testElement -> componentDependencies .modulesBuilder() .uninstalledTestDepsBuilder() .putAll( ClassName.get(testElement), getUninstalledModules(testElement, elements))); return componentDependencies.build(elements); } private static ImmutableMap descriptorLookupMap( ImmutableSet descriptors) { ImmutableMap.Builder builder = ImmutableMap.builder(); for (ComponentDescriptor descriptor : descriptors) { // This is a temporary hack to map the old ApplicationComponent to the new SingletonComponent. // Technically, this is only needed for backwards compatibility with libraries using the old // processor since new processors should convert to the new SingletonComponent when generating // the metadata class. if (descriptor.component().equals(ClassNames.SINGLETON_COMPONENT)) { builder.put("dagger.hilt.android.components.ApplicationComponent", descriptor); } builder.put(descriptor.component().toString(), descriptor); } return builder.build(); } // Validate that the @UninstallModules doesn't contain any test modules. private static Dependencies validateModules(Dependencies moduleDeps, Elements elements) { SetMultimap invalidTestModules = HashMultimap.create(); moduleDeps.testDeps().entries().stream() .filter( e -> moduleDeps.uninstalledTestDeps().containsEntry(e.getKey().test(), e.getValue())) .forEach(e -> invalidTestModules.put(e.getKey().test(), e.getValue())); // Currently we don't have a good way to throw an error for all tests, so we sort (to keep the // error reporting order stable) and then choose the first test. // TODO(bcorso): Consider using ProcessorErrorHandler directly to report all errors at once? Optional invalidTest = invalidTestModules.keySet().stream() .min((test1, test2) -> test1.toString().compareTo(test2.toString())); if (invalidTest.isPresent()) { throw new BadInputException( String.format( "@UninstallModules on test, %s, should not containing test modules, " + "but found: %s", invalidTest.get(), invalidTestModules.get(invalidTest.get()).stream() // Sort modules to keep stable error messages. .sorted((test1, test2) -> test1.toString().compareTo(test2.toString())) .collect(toImmutableList())), elements.getTypeElement(invalidTest.get().toString())); } return moduleDeps; } private static ImmutableSet getUninstalledModules( TypeElement testElement, Elements elements) { ImmutableList userUninstallModules = Processors.getAnnotationClassValues( elements, Processors.getAnnotationMirror(testElement, ClassNames.IGNORE_MODULES), "value"); // For pkg-private modules, find the generated wrapper class and uninstall that instead. return userUninstallModules.stream() .map(uninstallModule -> getPublicDependency(uninstallModule, elements)) .collect(toImmutableSet()); } /** Returns the public Hilt wrapper module, or the module itself if its already public. */ private static TypeElement getPublicDependency(TypeElement dependency, Elements elements) { return PkgPrivateMetadata.of(elements, dependency, ClassNames.MODULE) .map(metadata -> elements.getTypeElement(metadata.generatedClassName().toString())) .orElse(dependency); } /** Returns the top-level elements of the aggregated deps package. */ private static ImmutableList getAggregatedDeps(Elements elements) { PackageElement packageElement = elements.getPackageElement(AGGREGATING_PACKAGE); checkState( packageElement != null, "Couldn't find package %s. Did you mark your @Module classes with @InstallIn annotations?", AGGREGATING_PACKAGE); List aggregatedDepsElements = packageElement.getEnclosedElements(); checkState( !aggregatedDepsElements.isEmpty(), "No dependencies found. Did you mark your @Module classes with @InstallIn annotations?"); ImmutableList.Builder builder = ImmutableList.builder(); for (Element element : aggregatedDepsElements) { ProcessorErrors.checkState( element.getKind() == ElementKind.CLASS, element, "Only classes may be in package %s. Did you add custom code in the package?", AGGREGATING_PACKAGE); AnnotationMirror aggregatedDeps = Processors.getAnnotationMirror(element, ClassNames.AGGREGATED_DEPS); ProcessorErrors.checkState( aggregatedDeps != null, element, "Classes in package %s must be annotated with @AggregatedDeps: %s. Found: %s.", AGGREGATING_PACKAGE, element.getSimpleName(), element.getAnnotationMirrors()); builder.add(aggregatedDeps); } return builder.build(); } @AutoValue abstract static class AggregatedDepMetadata { static AggregatedDepMetadata create( AnnotationMirror aggregatedDeps, Map descriptorLookup, Elements elements) { ImmutableMap aggregatedDepsValues = Processors.getAnnotationValues(elements, aggregatedDeps); return new AutoValue_ComponentDependencies_AggregatedDepMetadata( getTestElement(aggregatedDepsValues.get("test"), elements), getComponents(aggregatedDepsValues.get("components"), descriptorLookup), getDependencyType( aggregatedDepsValues.get("modules"), aggregatedDepsValues.get("entryPoints"), aggregatedDepsValues.get("componentEntryPoints")), getDependency( aggregatedDepsValues.get("modules"), aggregatedDepsValues.get("entryPoints"), aggregatedDepsValues.get("componentEntryPoints"), elements), getReplacedDependencies(aggregatedDepsValues.get("replaces"), elements)); } enum DependencyType { MODULE, ENTRY_POINT, COMPONENT_ENTRY_POINT } abstract Optional testElement(); abstract ImmutableList componentDescriptors(); abstract DependencyType dependencyType(); abstract TypeElement dependency(); abstract ImmutableSet replacedDependencies(); private static Optional getTestElement( AnnotationValue testValue, Elements elements) { checkNotNull(testValue); String test = AnnotationValues.getString(testValue); return test.isEmpty() ? Optional.empty() : Optional.of(elements.getTypeElement(test)); } private static ImmutableList getComponents( AnnotationValue componentsValue, Map descriptorLookup) { checkNotNull(componentsValue); ImmutableList componentNames = AnnotationValues.getAnnotationValues(componentsValue).stream() .map(AnnotationValues::getString) .collect(toImmutableList()); checkState(!componentNames.isEmpty()); ImmutableList.Builder components = ImmutableList.builder(); for (String componentName : componentNames) { checkState( descriptorLookup.containsKey(componentName), "%s is not a valid Component. Did you add or remove code in package %s?", componentName, AGGREGATING_PACKAGE); components.add(descriptorLookup.get(componentName)); } return components.build(); } private static DependencyType getDependencyType( AnnotationValue modulesValue, AnnotationValue entryPointsValue, AnnotationValue componentEntryPointsValue) { checkNotNull(modulesValue); checkNotNull(entryPointsValue); checkNotNull(componentEntryPointsValue); ImmutableSet.Builder dependencyTypes = ImmutableSet.builder(); if (!AnnotationValues.getAnnotationValues(modulesValue).isEmpty()) { dependencyTypes.add(DependencyType.MODULE); } if (!AnnotationValues.getAnnotationValues(entryPointsValue).isEmpty()) { dependencyTypes.add(DependencyType.ENTRY_POINT); } if (!AnnotationValues.getAnnotationValues(componentEntryPointsValue).isEmpty()) { dependencyTypes.add(DependencyType.COMPONENT_ENTRY_POINT); } return getOnlyElement(dependencyTypes.build()); } private static TypeElement getDependency( AnnotationValue modulesValue, AnnotationValue entryPointsValue, AnnotationValue componentEntryPointsValue, Elements elements) { checkNotNull(modulesValue); checkNotNull(entryPointsValue); checkNotNull(componentEntryPointsValue); return elements.getTypeElement( AnnotationValues.getString( getOnlyElement( ImmutableList.builder() .addAll(AnnotationValues.getAnnotationValues(modulesValue)) .addAll(AnnotationValues.getAnnotationValues(entryPointsValue)) .addAll(AnnotationValues.getAnnotationValues(componentEntryPointsValue)) .build()))); } private static ImmutableSet getReplacedDependencies( AnnotationValue replacedDependenciesValue, Elements elements) { // Allow null values to support libraries using a Hilt version before @TestInstallIn was added return replacedDependenciesValue == null ? ImmutableSet.of() : AnnotationValues.getAnnotationValues(replacedDependenciesValue).stream() .map(AnnotationValues::getString) .map(elements::getTypeElement) .map(replacedDep -> getPublicDependency(replacedDep, elements)) .collect(toImmutableSet()); } } }