From d8284724abf82dfa13f3d505251eacc83ebb52bf Mon Sep 17 00:00:00 2001 From: Brad Corso Date: Mon, 11 Jan 2021 12:40:51 -0800 Subject: [PATCH] Add TestInstallIn to Hilt. This feature allows users to replace @InstallIn modules with a @TestInstallIn module. For example, you can replace FooModule with FakeFooModule with the following: ``` @Module @TestInstallIn( components = SingletonComponent.class, replaces = FooModule.class) interface FakeFooModule { ... } ``` Note that @TestInstallIn only replaces @InstallIn when using @HiltAndroidTest, it does not apply in @HiltAndroidApp. RELNOTES=Adds @TestInstallIn feature to Hilt PiperOrigin-RevId: 351213437 --- java/dagger/hilt/BUILD | 8 + .../lifecycle/DefaultViewModelFactories.java | 6 +- .../lifecycle/HiltViewModelFactory.java | 4 +- java/dagger/hilt/android/processor/BUILD | 5 +- java/dagger/hilt/android/testing/BUILD | 4 + java/dagger/hilt/processor/BUILD | 5 +- .../processor/internal/AnnotationValues.java | 6 +- .../hilt/processor/internal/ClassNames.java | 3 + .../hilt/processor/internal/Components.java | 25 +- .../aggregateddeps/AggregatedDeps.java | 3 + .../AggregatedDepsGenerator.java | 32 +- .../AggregatedDepsProcessor.java | 354 +++++++++++----- .../processor/internal/aggregateddeps/BUILD | 2 - .../aggregateddeps/ComponentDependencies.java | 387 ++++++++++++------ .../aggregateddeps/PkgPrivateMetadata.java | 2 + .../hilt/processor/internal/root/Root.java | 4 + .../processor/internal/root/RootMetadata.java | 5 +- java/dagger/hilt/testing/BUILD | 47 +++ java/dagger/hilt/testing/TestInstallIn.java | 57 +++ java/dagger/hilt/testing/package-info.java | 25 ++ .../processor/internal/aggregateddeps/BUILD | 56 +++ .../aggregateddeps/InstallInModule.java | 30 ++ .../aggregateddeps/TestInstallInTest.java | 365 +++++++++++++++++ 23 files changed, 1165 insertions(+), 270 deletions(-) create mode 100644 java/dagger/hilt/testing/BUILD create mode 100644 java/dagger/hilt/testing/TestInstallIn.java create mode 100644 java/dagger/hilt/testing/package-info.java create mode 100644 javatests/dagger/hilt/android/processor/internal/aggregateddeps/BUILD create mode 100644 javatests/dagger/hilt/android/processor/internal/aggregateddeps/InstallInModule.java create mode 100644 javatests/dagger/hilt/android/processor/internal/aggregateddeps/TestInstallInTest.java diff --git a/java/dagger/hilt/BUILD b/java/dagger/hilt/BUILD index bdb5793afad..94af18a9113 100644 --- a/java/dagger/hilt/BUILD +++ b/java/dagger/hilt/BUILD @@ -103,6 +103,7 @@ filegroup( ":hilt_android_filegroup", ":hilt_android_testing_filegroup", ":hilt_filegroup", + ":hilt_testing_filegroup", ], ) @@ -119,6 +120,13 @@ filegroup( ], ) +filegroup( + name = "hilt_testing_filegroup", + srcs = [ + "//java/dagger/hilt/testing:srcs_filegroup", + ], +) + filegroup( name = "hilt_android_filegroup", srcs = [ diff --git a/java/dagger/hilt/android/internal/lifecycle/DefaultViewModelFactories.java b/java/dagger/hilt/android/internal/lifecycle/DefaultViewModelFactories.java index 2f5e4ec506c..915f9934d6b 100644 --- a/java/dagger/hilt/android/internal/lifecycle/DefaultViewModelFactories.java +++ b/java/dagger/hilt/android/internal/lifecycle/DefaultViewModelFactories.java @@ -135,7 +135,7 @@ private static ViewModelProvider.Factory getFactoryFromSet(Set viewModelKeys(); @@ -152,14 +152,14 @@ public interface ActivityModule { /** The activity entry point to retrieve the factory. */ @EntryPoint @InstallIn(ActivityComponent.class) - public interface ActivityEntryPoint { + interface ActivityEntryPoint { InternalFactoryFactory getHiltInternalFactoryFactory(); } /** The fragment entry point to retrieve the factory. */ @EntryPoint @InstallIn(FragmentComponent.class) - public interface FragmentEntryPoint { + interface FragmentEntryPoint { InternalFactoryFactory getHiltInternalFactoryFactory(); } diff --git a/java/dagger/hilt/android/internal/lifecycle/HiltViewModelFactory.java b/java/dagger/hilt/android/internal/lifecycle/HiltViewModelFactory.java index b10de5cdd46..a47611d9195 100644 --- a/java/dagger/hilt/android/internal/lifecycle/HiltViewModelFactory.java +++ b/java/dagger/hilt/android/internal/lifecycle/HiltViewModelFactory.java @@ -56,10 +56,10 @@ interface ViewModelFactoriesEntryPoint { /** Hilt module for providing the empty multi-binding map of ViewModels. */ @Module @InstallIn(ViewModelComponent.class) - public abstract static class ViewModelModule { + interface ViewModelModule { @Multibinds @HiltViewModelMap - abstract Map hiltViewModelMap(); + Map hiltViewModelMap(); } private final Set viewModelInjectKeys; diff --git a/java/dagger/hilt/android/processor/BUILD b/java/dagger/hilt/android/processor/BUILD index ef31c1b9140..e116c4443cb 100644 --- a/java/dagger/hilt/android/processor/BUILD +++ b/java/dagger/hilt/android/processor/BUILD @@ -43,11 +43,9 @@ gen_maven_artifact( "//java/dagger/hilt/android/processor/internal/androidentrypoint:processor_lib", "//java/dagger/hilt/android/processor/internal/bindvalue:bind_value_processor_lib", "//java/dagger/hilt/android/processor/internal/customtestapplication:processor_lib", + "//java/dagger/hilt/android/processor/internal/viewmodel:validation_plugin_lib", "//java/dagger/hilt/android/processor/internal/uninstallmodules:processor_lib", "//java/dagger/hilt/android/processor/internal/viewmodel:processor_lib", - "//java/dagger/hilt/android/processor/internal/viewmodel:validation_plugin_lib", - "//java/dagger/hilt/codegen:originating_element", - "//java/dagger/hilt/codegen:package_info", "//java/dagger/hilt/processor/internal:base_processor", "//java/dagger/hilt/processor/internal:classnames", "//java/dagger/hilt/processor/internal:component_descriptor", @@ -56,7 +54,6 @@ gen_maven_artifact( "//java/dagger/hilt/processor/internal:kotlin", "//java/dagger/hilt/processor/internal:processor_errors", "//java/dagger/hilt/processor/internal:processors", - "//java/dagger/hilt/processor/internal/aggregateddeps:annotation", "//java/dagger/hilt/processor/internal/aggregateddeps:component_dependencies", "//java/dagger/hilt/processor/internal/aggregateddeps:processor_lib", "//java/dagger/hilt/processor/internal/aliasof:alias_ofs", diff --git a/java/dagger/hilt/android/testing/BUILD b/java/dagger/hilt/android/testing/BUILD index 069f9060e65..47db287e56e 100644 --- a/java/dagger/hilt/android/testing/BUILD +++ b/java/dagger/hilt/android/testing/BUILD @@ -171,6 +171,7 @@ android_library( ":package_info", ":uninstall_modules", "//java/dagger/hilt/android:artifact-lib", + "//java/dagger/hilt/testing:test_install_in", ], ) @@ -193,6 +194,8 @@ gen_maven_artifact( "//java/dagger/hilt/android/testing:hilt_test_application", "//java/dagger/hilt/android/testing:on_component_ready_runner", "//java/dagger/hilt/android/testing:package_info", + "//java/dagger/hilt/testing:test_install_in", + "//java/dagger/hilt/testing:package_info", "//java/dagger/hilt/android/testing:uninstall_modules", ], artifact_target_maven_deps = [ @@ -223,6 +226,7 @@ gen_maven_artifact( ], javadoc_srcs = [ "//java/dagger/hilt:hilt_android_testing_filegroup", + "//java/dagger/hilt:hilt_testing_filegroup", ], manifest = "AndroidManifest.xml", packaging = "aar", diff --git a/java/dagger/hilt/processor/BUILD b/java/dagger/hilt/processor/BUILD index 5d792af876d..87adcf34f65 100644 --- a/java/dagger/hilt/processor/BUILD +++ b/java/dagger/hilt/processor/BUILD @@ -63,11 +63,9 @@ gen_maven_artifact( "//java/dagger/hilt/android/processor/internal/androidentrypoint:processor_lib", "//java/dagger/hilt/android/processor/internal/bindvalue:bind_value_processor_lib", "//java/dagger/hilt/android/processor/internal/customtestapplication:processor_lib", + "//java/dagger/hilt/android/processor/internal/viewmodel:validation_plugin_lib", "//java/dagger/hilt/android/processor/internal/uninstallmodules:processor_lib", "//java/dagger/hilt/android/processor/internal/viewmodel:processor_lib", - "//java/dagger/hilt/android/processor/internal/viewmodel:validation_plugin_lib", - "//java/dagger/hilt/codegen:originating_element", - "//java/dagger/hilt/codegen:package_info", "//java/dagger/hilt/processor/internal:base_processor", "//java/dagger/hilt/processor/internal:classnames", "//java/dagger/hilt/processor/internal:component_descriptor", @@ -76,7 +74,6 @@ gen_maven_artifact( "//java/dagger/hilt/processor/internal:kotlin", "//java/dagger/hilt/processor/internal:processor_errors", "//java/dagger/hilt/processor/internal:processors", - "//java/dagger/hilt/processor/internal/aggregateddeps:annotation", "//java/dagger/hilt/processor/internal/aggregateddeps:component_dependencies", "//java/dagger/hilt/processor/internal/aggregateddeps:processor_lib", "//java/dagger/hilt/processor/internal/aliasof:alias_ofs", diff --git a/java/dagger/hilt/processor/internal/AnnotationValues.java b/java/dagger/hilt/processor/internal/AnnotationValues.java index af1234c8c3a..584d8f9505a 100644 --- a/java/dagger/hilt/processor/internal/AnnotationValues.java +++ b/java/dagger/hilt/processor/internal/AnnotationValues.java @@ -142,14 +142,14 @@ public static Optional getOptionalStringValue( /** Returns the int array value of an annotation */ public static int[] getIntArrayValue(AnnotationMirror annotation, String valueName) { - return asAnnotationValues(getAnnotationValue(annotation, valueName)).stream() + return getAnnotationValues(getAnnotationValue(annotation, valueName)).stream() .mapToInt(it -> (int) it.getValue()) .toArray(); } /** Returns the String array value of an annotation */ public static String[] getStringArrayValue(AnnotationMirror annotation, String valueName) { - return asAnnotationValues(getAnnotationValue(annotation, valueName)).stream() + return getAnnotationValues(getAnnotationValue(annotation, valueName)).stream() .map(it -> (String) it.getValue()) .toArray(String[]::new); } @@ -164,7 +164,7 @@ private static boolean isValuePresent(AnnotationMirror annotation, String valueN * * @throws IllegalArgumentException unless {@code annotationValue} represents an array */ - private static ImmutableList asAnnotationValues( + public static ImmutableList getAnnotationValues( AnnotationValue annotationValue) { return annotationValue.accept(AS_ANNOTATION_VALUES, null); } diff --git a/java/dagger/hilt/processor/internal/ClassNames.java b/java/dagger/hilt/processor/internal/ClassNames.java index de32055a074..234ea7b4588 100644 --- a/java/dagger/hilt/processor/internal/ClassNames.java +++ b/java/dagger/hilt/processor/internal/ClassNames.java @@ -24,6 +24,8 @@ public final class ClassNames { public static final ClassName ORIGINATING_ELEMENT = get("dagger.hilt.codegen", "OriginatingElement"); + public static final ClassName AGGREGATED_DEPS = + get("dagger.hilt.processor.internal.aggregateddeps", "AggregatedDeps"); public static final ClassName GENERATED_COMPONENT = get("dagger.hilt.internal", "GeneratedComponent"); public static final ClassName GENERATED_COMPONENT_MANAGER = @@ -91,6 +93,7 @@ public final class ClassNames { public static final ClassName INSTALL_IN = get("dagger.hilt", "InstallIn"); + public static final ClassName TEST_INSTALL_IN = get("dagger.hilt.testing", "TestInstallIn"); public static final ClassName ENTRY_POINT = get("dagger.hilt", "EntryPoint"); public static final ClassName ENTRY_POINTS = get("dagger.hilt", "EntryPoints"); diff --git a/java/dagger/hilt/processor/internal/Components.java b/java/dagger/hilt/processor/internal/Components.java index 3b097d12f85..661dd8a9bc9 100644 --- a/java/dagger/hilt/processor/internal/Components.java +++ b/java/dagger/hilt/processor/internal/Components.java @@ -24,7 +24,6 @@ import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; import dagger.hilt.processor.internal.definecomponent.DefineComponents; -import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.lang.model.util.Elements; @@ -49,7 +48,8 @@ public static ImmutableSet getComponentDescriptors( /** Returns the {@link dagger.hilt.InstallIn} components for a given element. */ public static ImmutableSet getComponents(Elements elements, Element element) { ImmutableSet components; - if (Processors.hasAnnotation(element, ClassNames.INSTALL_IN)) { + if (Processors.hasAnnotation(element, ClassNames.INSTALL_IN) + || Processors.hasAnnotation(element, ClassNames.TEST_INSTALL_IN)) { components = getHiltInstallInComponents(elements, element); } else { // Check the enclosing element in case it passed in module is a companion object. This helps @@ -88,20 +88,33 @@ public static AnnotationSpec getInstallInAnnotationSpec(ImmutableSet private static ImmutableSet getHiltInstallInComponents( Elements elements, Element element) { - AnnotationMirror hiltInstallIn = - Processors.getAnnotationMirror(element, ClassNames.INSTALL_IN); + Preconditions.checkArgument( + Processors.hasAnnotation(element, ClassNames.INSTALL_IN) + || Processors.hasAnnotation(element, ClassNames.TEST_INSTALL_IN)); + ImmutableSet components = - Processors.getAnnotationClassValues(elements, hiltInstallIn, "value").stream() - .collect(toImmutableSet()); + ImmutableSet.copyOf( + Processors.hasAnnotation(element, ClassNames.INSTALL_IN) + ? Processors.getAnnotationClassValues( + elements, + Processors.getAnnotationMirror(element, ClassNames.INSTALL_IN), + "value") + : Processors.getAnnotationClassValues( + elements, + Processors.getAnnotationMirror(element, ClassNames.TEST_INSTALL_IN), + "components")); + ImmutableSet undefinedComponents = components.stream() .filter(component -> !Processors.hasAnnotation(component, ClassNames.DEFINE_COMPONENT)) .collect(toImmutableSet()); + ProcessorErrors.checkState( undefinedComponents.isEmpty(), element, "@InstallIn, can only be used with @DefineComponent-annotated classes, but found: %s", undefinedComponents); + return components.stream().map(ClassName::get).collect(toImmutableSet()); } diff --git a/java/dagger/hilt/processor/internal/aggregateddeps/AggregatedDeps.java b/java/dagger/hilt/processor/internal/aggregateddeps/AggregatedDeps.java index 5792038e370..cc955ef39d1 100644 --- a/java/dagger/hilt/processor/internal/aggregateddeps/AggregatedDeps.java +++ b/java/dagger/hilt/processor/internal/aggregateddeps/AggregatedDeps.java @@ -31,6 +31,9 @@ /** Returns the test this dependency is associated with, otherwise an empty string. */ String test() default ""; + /** Returns the deps that this dep replaces. */ + String[] replaces() default {}; + String[] modules() default {}; String[] entryPoints() default {}; diff --git a/java/dagger/hilt/processor/internal/aggregateddeps/AggregatedDepsGenerator.java b/java/dagger/hilt/processor/internal/aggregateddeps/AggregatedDepsGenerator.java index 05b8dc737e1..bcc2dfe040b 100644 --- a/java/dagger/hilt/processor/internal/aggregateddeps/AggregatedDepsGenerator.java +++ b/java/dagger/hilt/processor/internal/aggregateddeps/AggregatedDepsGenerator.java @@ -16,18 +16,15 @@ package dagger.hilt.processor.internal.aggregateddeps; -import com.google.auto.common.MoreElements; import com.google.common.collect.ImmutableSet; import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.JavaFile; import com.squareup.javapoet.TypeSpec; -import dagger.hilt.processor.internal.ClassNames; import dagger.hilt.processor.internal.Processors; import java.io.IOException; import java.util.Optional; import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; /** @@ -41,17 +38,23 @@ final class AggregatedDepsGenerator { private final String dependencyType; private final TypeElement dependency; + private final Optional testName; private final ImmutableSet components; + private final ImmutableSet replacedDependencies; private final ProcessingEnvironment processingEnv; AggregatedDepsGenerator( String dependencyType, TypeElement dependency, + Optional testName, ImmutableSet components, + ImmutableSet replacedDependencies, ProcessingEnvironment processingEnv) { this.dependencyType = dependencyType; this.dependency = dependency; + this.testName = testName; this.components = components; + this.replacedDependencies = replacedDependencies; this.processingEnv = processingEnv; } @@ -75,28 +78,9 @@ void generate() throws IOException { private AnnotationSpec aggregatedDepsAnnotation() { AnnotationSpec.Builder annotationBuilder = AnnotationSpec.builder(AGGREGATED_DEPS); components.forEach(component -> annotationBuilder.addMember("components", "$S", component)); - getEnclosingTestName(dependency) - .ifPresent(test -> annotationBuilder.addMember("test", "$S", test)); + replacedDependencies.forEach(dep -> annotationBuilder.addMember("replaces", "$S", dep)); + testName.ifPresent(test -> annotationBuilder.addMember("test", "$S", test)); annotationBuilder.addMember(dependencyType, "$S", dependency.getQualifiedName()); return annotationBuilder.build(); } - - private Optional getEnclosingTestName(Element element) { - TypeElement topLevelType = getOriginatingTopLevelType(element); - return Processors.hasAnnotation(topLevelType, ClassNames.HILT_ANDROID_TEST) - ? Optional.of(ClassName.get(MoreElements.asType(topLevelType))) - : Optional.empty(); - } - - private TypeElement getOriginatingTopLevelType(Element element) { - TypeElement topLevelType = Processors.getTopLevelType(element); - if (Processors.hasAnnotation(topLevelType, ClassNames.ORIGINATING_ELEMENT)) { - return getOriginatingTopLevelType( - Processors.getAnnotationClassValue( - processingEnv.getElementUtils(), - Processors.getAnnotationMirror(topLevelType, ClassNames.ORIGINATING_ELEMENT), - "topLevelClass")); - } - return topLevelType; - } } diff --git a/java/dagger/hilt/processor/internal/aggregateddeps/AggregatedDepsProcessor.java b/java/dagger/hilt/processor/internal/aggregateddeps/AggregatedDepsProcessor.java index fb51e83703b..58c938f8830 100644 --- a/java/dagger/hilt/processor/internal/aggregateddeps/AggregatedDepsProcessor.java +++ b/java/dagger/hilt/processor/internal/aggregateddeps/AggregatedDepsProcessor.java @@ -18,6 +18,7 @@ import static com.google.auto.common.AnnotationMirrors.getAnnotationValue; import static com.google.auto.common.MoreElements.asType; +import static com.google.auto.common.MoreElements.getPackage; import static com.google.common.collect.Iterables.getOnlyElement; import static dagger.hilt.android.processor.internal.androidentrypoint.HiltCompilerOptions.BooleanOption.DISABLE_MODULES_HAVE_INSTALL_IN_CHECK; import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList; @@ -32,7 +33,6 @@ import com.google.auto.service.AutoService; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; import com.squareup.javapoet.ClassName; import dagger.hilt.processor.internal.BaseProcessor; import dagger.hilt.processor.internal.ClassNames; @@ -68,13 +68,20 @@ public final class AggregatedDepsProcessor extends BaseProcessor { ClassNames.GENERATED_ENTRY_POINT, ClassNames.COMPONENT_ENTRY_POINT); + private static final ImmutableSet MODULE_ANNOTATIONS = + ImmutableSet.of( + ClassNames.MODULE); + + private static final ImmutableSet INSTALL_IN_ANNOTATIONS = + ImmutableSet.of(ClassNames.INSTALL_IN, ClassNames.TEST_INSTALL_IN); + private final Set seen = new HashSet<>(); @Override public Set getSupportedAnnotationTypes() { return ImmutableSet.builder() - .add(ClassNames.MODULE) - .add(ClassNames.INSTALL_IN) + .addAll(INSTALL_IN_ANNOTATIONS) + .addAll(MODULE_ANNOTATIONS) .addAll(ENTRY_POINT_ANNOTATIONS) .build() .stream() @@ -88,135 +95,292 @@ public void processEach(TypeElement annotation, Element element) throws Exceptio return; } - ImmutableSet entryPointAnnotations = - ENTRY_POINT_ANNOTATIONS.stream() - .filter(entryPoint -> Processors.hasAnnotation(element, entryPoint)) - .collect(toImmutableSet()); - ProcessorErrors.checkState( - entryPointAnnotations.size() <= 1, - element, - "Found multiple @EntryPoint annotations on %s: %s", - element, - entryPointAnnotations); + Optional installInAnnotation = getAnnotation(element, INSTALL_IN_ANNOTATIONS); + Optional entryPointAnnotation = getAnnotation(element, ENTRY_POINT_ANNOTATIONS); + Optional moduleAnnotation = getAnnotation(element, MODULE_ANNOTATIONS); + + boolean hasInstallIn = installInAnnotation.isPresent(); + boolean isEntryPoint = entryPointAnnotation.isPresent(); + boolean isModule = moduleAnnotation.isPresent(); - boolean hasInstallIn = Processors.hasAnnotation(element, ClassNames.INSTALL_IN); - boolean isEntryPoint = !entryPointAnnotations.isEmpty(); - boolean isModule = Processors.hasAnnotation(element, ClassNames.MODULE); ProcessorErrors.checkState( !hasInstallIn || isEntryPoint || isModule, element, - "@InstallIn can only be used on @Module or @EntryPoint classes: %s", + "@%s-annotated classes must also be annotated with @Module or @EntryPoint: %s", + installInAnnotation.map(ClassName::simpleName).orElse("@InstallIn"), element); ProcessorErrors.checkState( - isModule != isEntryPoint, + !(isEntryPoint && isModule), element, - "@Module and @EntryPoint cannot be used on the same interface"); + "@%s and @%s cannot be used on the same interface: %s", + moduleAnnotation.map(ClassName::simpleName).orElse("@Module"), + entryPointAnnotation.map(ClassName::simpleName).orElse("@EntryPoint"), + element); + if (isModule) { + processModule(element, installInAnnotation, moduleAnnotation.get()); + } else if (isEntryPoint) { + processEntryPoint(element, installInAnnotation, entryPointAnnotation.get()); + } else { + throw new AssertionError(); + } + } + + private void processModule( + Element element, Optional installInAnnotation, ClassName moduleAnnotation) + throws Exception { ProcessorErrors.checkState( - !isModule - || hasInstallIn + installInAnnotation.isPresent() || isDaggerGeneratedModule(element) || installInCheckDisabled(element), element, - "%s is missing an @InstallIn annotation. If this was intentional, see " - + "https://dagger.dev/hilt/compiler-options#disable-install-in-check for how to disable this check.", + "%s is missing an @InstallIn annotation. If this was intentional, see" + + " https://dagger.dev/hilt/compiler-options#disable-install-in-check for how to disable this" + + " check.", + element); + + if (!installInAnnotation.isPresent()) { + // Modules without @InstallIn or @TestInstallIn annotations don't need to be processed further + return; + } + + ProcessorErrors.checkState( + element.getKind() == CLASS || element.getKind() == INTERFACE, + element, + "Only classes and interfaces can be annotated with @Module: %s", element); + TypeElement module = asType(element); - if (isModule && hasInstallIn) { + ProcessorErrors.checkState( + Processors.isTopLevel(module) + || module.getModifiers().contains(STATIC) + || module.getModifiers().contains(ABSTRACT) + || Processors.hasAnnotation(module.getEnclosingElement(), ClassNames.HILT_ANDROID_TEST), + module, + "Nested @%s modules must be static unless they are directly nested within a test. " + + "Found: %s", + installInAnnotation.get().simpleName(), + module); + + // Check that if Dagger needs an instance of the module, Hilt can provide it automatically by + // calling a visible empty constructor. + ProcessorErrors.checkState( + !daggerRequiresModuleInstance(module) || hasVisibleEmptyConstructor(module), + module, + "Modules that need to be instantiated by Hilt must have a visible, empty constructor."); + + // TODO(b/28989613): This should really be fixed in Dagger. Remove once Dagger bug is fixed. + ImmutableList abstractMethodsWithMissingBinds = + ElementFilter.methodsIn(module.getEnclosedElements()).stream() + .filter(method -> method.getModifiers().contains(ABSTRACT)) + .filter(method -> !Processors.hasDaggerAbstractMethodAnnotation(method)) + .collect(toImmutableList()); + ProcessorErrors.checkState( + abstractMethodsWithMissingBinds.isEmpty(), + module, + "Found unimplemented abstract methods, %s, in an abstract module, %s. " + + "Did you forget to add a Dagger binding annotation (e.g. @Binds)?", + abstractMethodsWithMissingBinds, + module); + + ImmutableList replacedModules = ImmutableList.of(); + if (Processors.hasAnnotation(module, ClassNames.TEST_INSTALL_IN)) { + Optional originatingTestElement = getOriginatingTestElement(module); ProcessorErrors.checkState( - element.getKind() == CLASS || element.getKind() == INTERFACE, - element, - "Only classes and interfaces can be annotated with @Module: %s", - element); - TypeElement module = asType(element); + !originatingTestElement.isPresent(), + // TODO(b/152801981): this should really error on the annotation value + module, + "@TestInstallIn modules cannot be nested in (or originate from) a " + + "@HiltAndroidTest-annotated class: %s", + originatingTestElement + .map(testElement -> testElement.getQualifiedName().toString()) + .orElse("")); + + AnnotationMirror testInstallIn = + Processors.getAnnotationMirror(module, ClassNames.TEST_INSTALL_IN); + replacedModules = + Processors.getAnnotationClassValues(getElementUtils(), testInstallIn, "replaces"); ProcessorErrors.checkState( - Processors.isTopLevel(module) - || module.getModifiers().contains(STATIC) - || module.getModifiers().contains(ABSTRACT) - || Processors.hasAnnotation( - module.getEnclosingElement(), ClassNames.HILT_ANDROID_TEST), + !replacedModules.isEmpty(), + // TODO(b/152801981): this should really error on the annotation value module, - "Nested @InstallIn modules must be static unless they are directly nested within a test. " - + "Found: %s", - module); + "@TestInstallIn#replaces() cannot be empty. Use @InstallIn instead."); + + ImmutableList nonInstallInModules = + replacedModules.stream() + .filter( + replacedModule -> + !Processors.hasAnnotation(replacedModule, ClassNames.INSTALL_IN)) + .collect(toImmutableList()); - // Check that if Dagger needs an instance of the module, Hilt can provide it automatically by - // calling a visible empty constructor. ProcessorErrors.checkState( - !daggerRequiresModuleInstance(module) || hasVisibleEmptyConstructor(module), + nonInstallInModules.isEmpty(), + // TODO(b/152801981): this should really error on the annotation value module, - "Modules that need to be instantiated by Hilt must have a visible, empty constructor."); + "@TestInstallIn#replaces() can only contain @InstallIn modules, but found: %s", + nonInstallInModules); - // TODO(b/28989613): This should really be fixed in Dagger. Remove once Dagger bug is fixed. - ImmutableList abstractMethodsWithMissingBinds = - ElementFilter.methodsIn(module.getEnclosedElements()).stream() - .filter(method -> method.getModifiers().contains(ABSTRACT)) - .filter(method -> !Processors.hasDaggerAbstractMethodAnnotation(method)) + ImmutableList hiltWrapperModules = + replacedModules.stream() + .filter( + replacedModule -> + replacedModule.getSimpleName().toString().startsWith("HiltWrapper_")) .collect(toImmutableList()); + ProcessorErrors.checkState( - abstractMethodsWithMissingBinds.isEmpty(), + hiltWrapperModules.isEmpty(), + // TODO(b/152801981): this should really error on the annotation value module, - "Found unimplemented abstract methods, %s, in an abstract module, %s. " - + "Did you forget to add a Dagger binding annotation (e.g. @Binds)?", - abstractMethodsWithMissingBinds, - module); - - // Get @InstallIn components here to catch errors before skipping user's pkg-private element. - ImmutableSet components = Components.getComponents(getElementUtils(), module); - if (isValidKind(module)) { - Optional pkgPrivateMetadata; - pkgPrivateMetadata = PkgPrivateMetadata.of(getElementUtils(), module, ClassNames.MODULE); - if (pkgPrivateMetadata.isPresent()) { - // Generate a public wrapper module which will be processed in the next round. - new PkgPrivateModuleGenerator(getProcessingEnv(), pkgPrivateMetadata.get()).generate(); - } else { - new AggregatedDepsGenerator("modules", module, components, getProcessingEnv()).generate(); - } + "@TestInstallIn#replaces() cannot contain Hilt generated public wrapper modules, " + + "but found: %s. ", + hiltWrapperModules); + + if (!getPackage(module).getQualifiedName().toString().startsWith("dagger.hilt")) { + // Prevent external users from overriding Hilt's internal modules. Techincally, except for + // ApplicationContextModule, making all modules pkg-private should be enough but this is an + // extra measure of precaution. + ImmutableList hiltInternalModules = + replacedModules.stream() + .filter( + replacedModule -> + getPackage(replacedModule) + .getQualifiedName() + .toString() + .startsWith("dagger.hilt")) + .collect(toImmutableList()); + + ProcessorErrors.checkState( + hiltInternalModules.isEmpty(), + // TODO(b/152801981): this should really error on the annotation value + module, + "@TestInstallIn#replaces() cannot contain internal Hilt modules, but found: %s. ", + hiltInternalModules); } - } - if (isEntryPoint) { - ClassName entryPointAnnotation = Iterables.getOnlyElement(entryPointAnnotations); + // Prevent users from uninstalling test-specific @InstallIn modules. + ImmutableList replacedTestSpecificInstallIn = + replacedModules.stream() + .filter(replacedModule -> getOriginatingTestElement(replacedModule).isPresent()) + .collect(toImmutableList()); ProcessorErrors.checkState( - hasInstallIn , - element, - "@%s %s must also be annotated with @InstallIn", - entryPointAnnotation.simpleName(), - element); + replacedTestSpecificInstallIn.isEmpty(), + // TODO(b/152801981): this should really error on the annotation value + module, + "@TestInstallIn#replaces() cannot replace test specific @InstallIn modules, but found: " + + "%s. Please remove the @InstallIn module manually rather than replacing it.", + replacedTestSpecificInstallIn); + } - ProcessorErrors.checkState( - element.getKind() == INTERFACE, - element, - "Only interfaces can be annotated with @%s: %s", - entryPointAnnotation.simpleName(), - element); - TypeElement entryPoint = asType(element); - - // Get @InstallIn components here to catch errors before skipping user's pkg-private element. - ImmutableSet components = Components.getComponents(getElementUtils(), entryPoint); - if (isValidKind(element)) { - if (entryPointAnnotation.equals(ClassNames.COMPONENT_ENTRY_POINT)) { - new AggregatedDepsGenerator( - "componentEntryPoints", entryPoint, components, getProcessingEnv()) - .generate(); + generateAggregatedDeps( + "modules", + module, + moduleAnnotation, + replacedModules.stream().map(ClassName::get).collect(toImmutableSet())); + } + + private void processEntryPoint( + Element element, Optional installInAnnotation, ClassName entryPointAnnotation) + throws Exception { + ProcessorErrors.checkState( + installInAnnotation.isPresent() , + element, + "@%s %s must also be annotated with @InstallIn", + entryPointAnnotation.simpleName(), + element); + + ProcessorErrors.checkState( + !Processors.hasAnnotation(element, ClassNames.TEST_INSTALL_IN), + element, + "@TestInstallIn can only be used with modules"); + + ProcessorErrors.checkState( + element.getKind() == INTERFACE, + element, + "Only interfaces can be annotated with @%s: %s", + entryPointAnnotation.simpleName(), + element); + TypeElement entryPoint = asType(element); + + generateAggregatedDeps( + entryPointAnnotation.equals(ClassNames.COMPONENT_ENTRY_POINT) + ? "componentEntryPoints" + : "entryPoints", + entryPoint, + entryPointAnnotation, + ImmutableSet.of()); + } + + private void generateAggregatedDeps( + String key, + TypeElement element, + ClassName annotation, + ImmutableSet replacedModules) + throws Exception { + // Get @InstallIn components here to catch errors before skipping user's pkg-private element. + ImmutableSet components = Components.getComponents(getElementUtils(), element); + + if (isValidKind(element)) { + Optional pkgPrivateMetadata = + PkgPrivateMetadata.of(getElementUtils(), element, annotation); + if (pkgPrivateMetadata.isPresent()) { + if (key.contentEquals("modules")) { + new PkgPrivateModuleGenerator(getProcessingEnv(), pkgPrivateMetadata.get()).generate(); } else { - Optional pkgPrivateMetadata = - PkgPrivateMetadata.of(getElementUtils(), entryPoint, entryPointAnnotation); - if (pkgPrivateMetadata.isPresent()) { - new PkgPrivateEntryPointGenerator(getProcessingEnv(), pkgPrivateMetadata.get()) - .generate(); - } else { - new AggregatedDepsGenerator("entryPoints", entryPoint, components, getProcessingEnv()) - .generate(); - } + new PkgPrivateEntryPointGenerator(getProcessingEnv(), pkgPrivateMetadata.get()) + .generate(); } + } else { + Optional testName = getOriginatingTestElement(element).map(ClassName::get); + new AggregatedDepsGenerator( + key, element, testName, components, replacedModules, getProcessingEnv()) + .generate(); } } } + private static Optional getAnnotation( + Element element, ImmutableSet annotations) { + ImmutableSet usedAnnotations = + annotations.stream() + .filter(annotation -> Processors.hasAnnotation(element, annotation)) + .collect(toImmutableSet()); + + if (usedAnnotations.isEmpty()) { + return Optional.empty(); + } + + ProcessorErrors.checkState( + usedAnnotations.size() == 1, + element, + "Only one of the following annotations can be used on %s: %s", + element, + usedAnnotations); + + return Optional.of(getOnlyElement(usedAnnotations)); + } + + private Optional getOriginatingTestElement(Element element) { + TypeElement topLevelType = getOriginatingTopLevelType(element); + return Processors.hasAnnotation(topLevelType, ClassNames.HILT_ANDROID_TEST) + ? Optional.of(asType(topLevelType)) + : Optional.empty(); + } + + private TypeElement getOriginatingTopLevelType(Element element) { + TypeElement topLevelType = Processors.getTopLevelType(element); + if (Processors.hasAnnotation(topLevelType, ClassNames.ORIGINATING_ELEMENT)) { + return getOriginatingTopLevelType( + Processors.getAnnotationClassValue( + getElementUtils(), + Processors.getAnnotationMirror(topLevelType, ClassNames.ORIGINATING_ELEMENT), + "topLevelClass")); + } + return topLevelType; + } + private static boolean isValidKind(Element element) { // don't go down the rabbit hole of analyzing undefined types. N.B. we don't issue // an error here because javac already has and we don't want to spam the user. diff --git a/java/dagger/hilt/processor/internal/aggregateddeps/BUILD b/java/dagger/hilt/processor/internal/aggregateddeps/BUILD index 1eb8d3631e5..ebbc941bcae 100644 --- a/java/dagger/hilt/processor/internal/aggregateddeps/BUILD +++ b/java/dagger/hilt/processor/internal/aggregateddeps/BUILD @@ -69,13 +69,11 @@ java_library( "ComponentDependencies.java", ], deps = [ - ":annotation", ":processor_lib", "//java/dagger/hilt/processor/internal:classnames", "//java/dagger/hilt/processor/internal:component_descriptor", "//java/dagger/hilt/processor/internal:processor_errors", "//java/dagger/hilt/processor/internal:processors", - "//java/dagger/hilt/processor/internal/definecomponent:define_components", "//java/dagger/internal/codegen/extension", "//java/dagger/internal/guava:base", "//java/dagger/internal/guava:collect", diff --git a/java/dagger/hilt/processor/internal/aggregateddeps/ComponentDependencies.java b/java/dagger/hilt/processor/internal/aggregateddeps/ComponentDependencies.java index dfee360a33c..23ed148c0ec 100644 --- a/java/dagger/hilt/processor/internal/aggregateddeps/ComponentDependencies.java +++ b/java/dagger/hilt/processor/internal/aggregateddeps/ComponentDependencies.java @@ -16,7 +16,9 @@ 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; @@ -29,25 +31,55 @@ 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 java.util.HashMap; +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) - */ -public final class ComponentDependencies { +/** 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 @@ -70,90 +102,59 @@ static TestDepKey of(ClassName component, ClassName test) { * 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. */ - private static final class Dependencies { - private static final class Builder { - private final ImmutableSetMultimap.Builder globalDeps = - ImmutableSetMultimap.builder(); - private final ImmutableSetMultimap.Builder testDeps = - ImmutableSetMultimap.builder(); - private final ImmutableSetMultimap.Builder ignoreDeps = - ImmutableSetMultimap.builder(); - - Builder addDep(ClassName component, Optional test, TypeElement dep) { - if (test.isPresent()) { - testDeps.put(TestDepKey.of(component, test.get()), dep); - } else { - globalDeps.put(component, dep); - } - return this; - } + @AutoValue + public abstract static class Dependencies { + static Builder builder() { + return new AutoValue_ComponentDependencies_Dependencies.Builder(); + } - Builder ignoreDeps(ClassName test, ImmutableSet deps) { - ignoreDeps.putAll(test, deps); - return this; - } + /** Returns the global deps keyed by component. */ + abstract ImmutableSetMultimap globalDeps(); - Dependencies build() { - return new Dependencies(globalDeps.build(), testDeps.build(), ignoreDeps.build()); - } - } + /** Returns the global test deps keyed by component. */ + abstract ImmutableSetMultimap globalTestDeps(); - // Stores global deps keyed by component. - private final ImmutableSetMultimap globalDeps; + /** Returns the test deps keyed by component and test. */ + abstract ImmutableSetMultimap testDeps(); - // Stores test deps keyed by component and test. - private final ImmutableSetMultimap testDeps; + /** Returns the uninstalled test deps keyed by test. */ + abstract ImmutableSetMultimap uninstalledTestDeps(); - // Stores ignored deps keyed by test. - private final ImmutableSetMultimap ignoreDeps; + /** Returns the global uninstalled test deps. */ + abstract ImmutableSet globalUninstalledTestDeps(); - Dependencies( - ImmutableSetMultimap globalDeps, - ImmutableSetMultimap testDeps, - ImmutableSetMultimap ignoreDeps) { - this.globalDeps = globalDeps; - this.testDeps = testDeps; - this.ignoreDeps = ignoreDeps; - } + /** 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); + } - /** Returns the dependencies to be installed in the given component for the given test. */ - ImmutableSet get(ClassName component, ClassName test) { - ImmutableSet ignoreTestDeps = ignoreDeps.get(test); + ImmutableSet uninstalledTestDepsForRoot = uninstalledTestDeps().get(root); return ImmutableSet.builder() .addAll( - globalDeps.get(component).stream() - .filter(dep -> !ignoreTestDeps.contains(dep)) + globalDeps().get(component).stream() + .filter(dep -> !uninstalledTestDepsForRoot.contains(dep)) + .filter(dep -> !globalUninstalledTestDeps().contains(dep)) .collect(toImmutableSet())) - .addAll(testDeps.get(TestDepKey.of(component, test))) + .addAll(globalTestDeps().get(component)) + .addAll(testDeps().get(TestDepKey.of(component, root))) .build(); } - } - private final Dependencies modules; - private final Dependencies entryPoints; - private final Dependencies componentEntryPoints; + @AutoValue.Builder + abstract static class Builder { + abstract ImmutableSetMultimap.Builder globalDepsBuilder(); - private ComponentDependencies( - Dependencies modules, Dependencies entryPoints, Dependencies componentEntryPoints) { - this.modules = modules; - this.entryPoints = entryPoints; - this.componentEntryPoints = componentEntryPoints; - } + abstract ImmutableSetMultimap.Builder globalTestDepsBuilder(); - /** Returns the modules for a component, without any filtering. */ - public ImmutableSet getModules(ClassName componentName, ClassName rootName) { - return modules.get(componentName, rootName); - } + abstract ImmutableSetMultimap.Builder testDepsBuilder(); - /** Returns the entry points associated with the given a component. */ - public ImmutableSet getEntryPoints(ClassName componentName, ClassName rootName) { - return entryPoints.get(componentName, rootName); - } + abstract ImmutableSetMultimap.Builder uninstalledTestDepsBuilder(); - /** Returns the component entry point associated with the given a component. */ - public ImmutableSet getComponentEntryPoints( - ClassName componentName, ClassName rootName) { - return componentEntryPoints.get(componentName, rootName); + abstract ImmutableSet.Builder globalUninstalledTestDepsBuilder(); + + abstract Dependencies build(); + } } /** @@ -174,50 +175,63 @@ public ImmutableSet getComponentEntryPoints( */ public static ComponentDependencies from( ImmutableSet descriptors, Elements elements) { - Dependencies.Builder moduleDeps = new Dependencies.Builder(); - Dependencies.Builder entryPointDeps = new Dependencies.Builder(); - Dependencies.Builder componentEntryPointDeps = new Dependencies.Builder(); - Map testElements = new HashMap<>(); Map descriptorLookup = descriptorLookupMap(descriptors); - - for (AggregatedDeps deps : getAggregatedDeps(elements)) { - Optional test = Optional.empty(); - if (!deps.test().isEmpty()) { - testElements.computeIfAbsent(deps.test(), testName -> elements.getTypeElement(testName)); - test = Optional.of(ClassName.get(testElements.get(deps.test()))); + 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 (String component : deps.components()) { - checkState( - descriptorLookup.containsKey(component), - "%s is not a valid Component. Did you add or remove code in package %s?", - component, - AGGREGATING_PACKAGE, - component); - - ComponentDescriptor desc = descriptorLookup.get(component); - for (String dep : deps.modules()) { - moduleDeps.addDep(desc.component(), test, elements.getTypeElement(dep)); - } - for (String dep : deps.entryPoints()) { - entryPointDeps.addDep(desc.component(), test, elements.getTypeElement(dep)); - } - for (String dep : deps.componentEntryPoints()) { - componentEntryPointDeps.addDep(desc.component(), test, elements.getTypeElement(dep)); + 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()); + } } } } - for (TypeElement testElement : testElements.values()) { - if (Processors.hasAnnotation(testElement, ClassNames.IGNORE_MODULES)) { - moduleDeps.ignoreDeps(ClassName.get(testElement), getIgnoredModules(testElement, elements)); - } - } - - return new ComponentDependencies( - validateModules(moduleDeps.build(), elements), - entryPointDeps.build(), - componentEntryPointDeps.build()); + // 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( @@ -239,8 +253,9 @@ private static ImmutableMap descriptorLookupMap( // 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.ignoreDeps.containsEntry(e.getKey().test(), e.getValue())) + 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 @@ -264,7 +279,7 @@ private static Dependencies validateModules(Dependencies moduleDeps, Elements el return moduleDeps; } - private static ImmutableSet getIgnoredModules( + private static ImmutableSet getUninstalledModules( TypeElement testElement, Elements elements) { ImmutableList userUninstallModules = Processors.getAnnotationClassValues( @@ -273,20 +288,20 @@ private static ImmutableSet getIgnoredModules( "value"); // For pkg-private modules, find the generated wrapper class and uninstall that instead. - ImmutableSet.Builder builder = ImmutableSet.builder(); - for (TypeElement ignoreModule : userUninstallModules) { - Optional pkgPrivateMetadata = - PkgPrivateMetadata.of(elements, ignoreModule, ClassNames.MODULE); - builder.add( - pkgPrivateMetadata.isPresent() - ? elements.getTypeElement(pkgPrivateMetadata.get().generatedClassName().toString()) - : ignoreModule); - } - return builder.build(); + 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) { + private static ImmutableList getAggregatedDeps(Elements elements) { PackageElement packageElement = elements.getPackageElement(AGGREGATING_PACKAGE); checkState( packageElement != null, @@ -298,7 +313,7 @@ private static ImmutableList getAggregatedDeps(Elements elements !aggregatedDepsElements.isEmpty(), "No dependencies found. Did you mark your @Module classes with @InstallIn annotations?"); - ImmutableList.Builder builder = ImmutableList.builder(); + ImmutableList.Builder builder = ImmutableList.builder(); for (Element element : aggregatedDepsElements) { ProcessorErrors.checkState( element.getKind() == ElementKind.CLASS, @@ -306,7 +321,8 @@ private static ImmutableList getAggregatedDeps(Elements elements "Only classes may be in package %s. Did you add custom code in the package?", AGGREGATING_PACKAGE); - AggregatedDeps aggregatedDeps = element.getAnnotation(AggregatedDeps.class); + AnnotationMirror aggregatedDeps = + Processors.getAnnotationMirror(element, ClassNames.AGGREGATED_DEPS); ProcessorErrors.checkState( aggregatedDeps != null, element, @@ -319,4 +335,125 @@ private static ImmutableList getAggregatedDeps(Elements elements } 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()); + } + } } diff --git a/java/dagger/hilt/processor/internal/aggregateddeps/PkgPrivateMetadata.java b/java/dagger/hilt/processor/internal/aggregateddeps/PkgPrivateMetadata.java index 0a4c22f5205..66f175151f3 100644 --- a/java/dagger/hilt/processor/internal/aggregateddeps/PkgPrivateMetadata.java +++ b/java/dagger/hilt/processor/internal/aggregateddeps/PkgPrivateMetadata.java @@ -72,6 +72,8 @@ static Optional of(Elements elements, Element element, Class Optional installIn; if (Processors.hasAnnotation(element, ClassNames.INSTALL_IN)) { installIn = Optional.of(Processors.getAnnotationMirror(element, ClassNames.INSTALL_IN)); + } else if (Processors.hasAnnotation(element, ClassNames.TEST_INSTALL_IN)) { + installIn = Optional.of(Processors.getAnnotationMirror(element, ClassNames.TEST_INSTALL_IN)); } else { throw new IllegalStateException( "Expected element to be annotated with @InstallIn: " + element); diff --git a/java/dagger/hilt/processor/internal/root/Root.java b/java/dagger/hilt/processor/internal/root/Root.java index f60a17d1445..981636d6265 100644 --- a/java/dagger/hilt/processor/internal/root/Root.java +++ b/java/dagger/hilt/processor/internal/root/Root.java @@ -47,4 +47,8 @@ ClassName classname() { public final String toString() { return element().toString(); } + + boolean isTestRoot() { + return type().isTestRoot(); + } } diff --git a/java/dagger/hilt/processor/internal/root/RootMetadata.java b/java/dagger/hilt/processor/internal/root/RootMetadata.java index 3dc7164c057..bf77ced3a33 100644 --- a/java/dagger/hilt/processor/internal/root/RootMetadata.java +++ b/java/dagger/hilt/processor/internal/root/RootMetadata.java @@ -93,7 +93,7 @@ public ComponentDependencies deps() { } public ImmutableSet modules(ClassName componentName) { - return deps.getModules(componentName, root.classname()); + return deps.modules().get(componentName, root.classname(), root.isTestRoot()); } public ImmutableSet entryPoints(ClassName componentName) { @@ -167,7 +167,8 @@ private void validate() { private ImmutableSet getUserDefinedEntryPoints(ClassName componentName) { ImmutableSet.Builder entryPointSet = ImmutableSet.builder(); entryPointSet.add(ClassNames.GENERATED_COMPONENT); - for (TypeElement element : deps.getEntryPoints(componentName, root.classname())) { + for (TypeElement element : + deps.entryPoints().get(componentName, root.classname(), root.isTestRoot())) { entryPointSet.add(ClassName.get(element)); } return entryPointSet.build(); diff --git a/java/dagger/hilt/testing/BUILD b/java/dagger/hilt/testing/BUILD new file mode 100644 index 00000000000..3ea1c5f4b4e --- /dev/null +++ b/java/dagger/hilt/testing/BUILD @@ -0,0 +1,47 @@ +# Copyright (C) 2020 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. +# +# Description: +# Testing libraries for Hilt. + +package(default_visibility = ["//:src"]) + +java_library( + name = "package_info", + srcs = ["package-info.java"], + deps = [ + "@google_bazel_common//third_party/java/jsr305_annotations", + ], +) + +java_library( + name = "test_install_in", + testonly = 1, + srcs = ["TestInstallIn.java"], + exported_plugins = [ + "//java/dagger/hilt/processor/internal/aggregateddeps:plugin", + ], + exports = [ + "//java/dagger/hilt/processor/internal/aggregateddeps:annotation", + ], + deps = [ + ":package_info", + "//java/dagger/hilt:generates_root_input", + ], +) + +filegroup( + name = "srcs_filegroup", + srcs = glob(["*"]), +) diff --git a/java/dagger/hilt/testing/TestInstallIn.java b/java/dagger/hilt/testing/TestInstallIn.java new file mode 100644 index 00000000000..dfe138988b8 --- /dev/null +++ b/java/dagger/hilt/testing/TestInstallIn.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2020 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.testing; + +import static java.lang.annotation.RetentionPolicy.CLASS; + +import dagger.hilt.GeneratesRootInput; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * An annotation that replaces one or more {@link dagger.hilt.InstallIn} modules with the annotated + * module in tests. + * + *

The annotated class must also be annotated with {@link dagger.Module}. + * + *

Example: + * + *


+ *   // Replaces FooModule with FakeFooModule, and installs it into the same component as FooModule.
+ *   {@literal @}Module
+ *   {@literal @}TestInstallIn(components = SingletonComponent.class, replaces = FooModule.class)
+ *   public final class FakeFooModule {
+ *     {@literal @}Provides
+ *     static Foo provideFoo() {
+ *       return new FakeFoo();
+ *     }
+ *   }
+ * 
+ * + * @see Hilt Modules + */ +@Retention(CLASS) +@Target({ElementType.TYPE}) +@GeneratesRootInput +public @interface TestInstallIn { + /** Returns the component(s) into which the annotated module will be installed. */ + Class[] components(); + + /** Returns the {@link InstallIn} module(s) that the annotated class will replace in tests. */ + Class[] replaces(); +} diff --git a/java/dagger/hilt/testing/package-info.java b/java/dagger/hilt/testing/package-info.java new file mode 100644 index 00000000000..5837343c9e2 --- /dev/null +++ b/java/dagger/hilt/testing/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2020 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. + */ + +/** + * This package contains APIs for writing tests with Hilt. + * + * @see Hilt Testing + */ +@ParametersAreNonnullByDefault +package dagger.hilt.testing; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/javatests/dagger/hilt/android/processor/internal/aggregateddeps/BUILD b/javatests/dagger/hilt/android/processor/internal/aggregateddeps/BUILD new file mode 100644 index 00000000000..df818b83837 --- /dev/null +++ b/javatests/dagger/hilt/android/processor/internal/aggregateddeps/BUILD @@ -0,0 +1,56 @@ +# Copyright (C) 2020 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. +# Description: +# Tests for internal code for implementing Hilt processors. + +load("//java/dagger/testing/compile:macros.bzl", "compiler_test") + +package(default_visibility = ["//:src"]) + +compiler_test( + name = "TestInstallInTest", + srcs = ["TestInstallInTest.java"], + compiler_deps = [ + ":InstallInModule", + "//:dagger_with_compiler", + "//java/dagger/hilt:install_in", + "//java/dagger/hilt:entry_point", + "//java/dagger/hilt/components", + "//java/dagger/hilt/android/internal/modules", + "//java/dagger/hilt/testing:test_install_in", + "//java/dagger/hilt/android/testing:hilt_android_test", + "@androidsdk//:platforms/android-30/android.jar", + ], + deps = [ + "//javatests/dagger/hilt/android/processor:android_compilers", + "@google_bazel_common//third_party/java/compile_testing", + "@google_bazel_common//third_party/java/junit", + "@google_bazel_common//third_party/java/truth", + ], +) + +java_library( + name = "InstallInModule", + srcs = ["InstallInModule.java"], + deps = [ + "//:dagger_with_compiler", + "//java/dagger/hilt:install_in", + "//java/dagger/hilt/components", + ], +) + +filegroup( + name = "srcs_filegroup", + srcs = glob(["*"]), +) diff --git a/javatests/dagger/hilt/android/processor/internal/aggregateddeps/InstallInModule.java b/javatests/dagger/hilt/android/processor/internal/aggregateddeps/InstallInModule.java new file mode 100644 index 00000000000..af535f58f5a --- /dev/null +++ b/javatests/dagger/hilt/android/processor/internal/aggregateddeps/InstallInModule.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 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.android.processor.internal.aggregateddeps; + +import dagger.Module; +import dagger.hilt.InstallIn; +import dagger.hilt.components.SingletonComponent; + +/** + * This is used in TestInstallInTest to test that the wrapper module, HiltWrapper_InstallInModule, + * cannot be replaced. This needs to be compiled in a separate library because + * AggregatedDepsProcesor does not defer modules that have not been generated yet. + */ +@Module +@InstallIn(SingletonComponent.class) +interface InstallInModule {} diff --git a/javatests/dagger/hilt/android/processor/internal/aggregateddeps/TestInstallInTest.java b/javatests/dagger/hilt/android/processor/internal/aggregateddeps/TestInstallInTest.java new file mode 100644 index 00000000000..3af5be6fd0f --- /dev/null +++ b/javatests/dagger/hilt/android/processor/internal/aggregateddeps/TestInstallInTest.java @@ -0,0 +1,365 @@ +/* + * Copyright (C) 2020 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.android.processor.internal.aggregateddeps; + +import static com.google.testing.compile.CompilationSubject.assertThat; +import static dagger.hilt.android.processor.AndroidCompilers.compiler; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class TestInstallInTest { + + @Test + public void testMissingValues() { + JavaFileObject testInstallInModule = + JavaFileObjects.forSourceLines( + "test.TestInstallInModule", + "package test;", + "", + "import dagger.Module;", + "import dagger.hilt.testing.TestInstallIn;", + "", + "@Module", + "@TestInstallIn", + "interface TestInstallInModule {}"); + Compilation compilation = compiler().compile(testInstallInModule); + assertThat(compilation).failed(); + assertThat(compilation).hadErrorCount(1); + assertThat(compilation) + .hadErrorContaining( + "@dagger.hilt.testing.TestInstallIn is missing default values for elements " + + "components,replaces") + .inFile(testInstallInModule) + .onLine(7); + } + + @Test + public void testEmptyComponentValues() { + JavaFileObject installInModule = + JavaFileObjects.forSourceLines( + "test.InstallInModule", + "package test;", + "", + "import dagger.Module;", + "import dagger.hilt.InstallIn;", + "import dagger.hilt.components.SingletonComponent;", + "", + "@Module", + "@InstallIn(SingletonComponent.class)", + "interface InstallInModule {}"); + JavaFileObject testInstallInModule = + JavaFileObjects.forSourceLines( + "test.TestInstallInModule", + "package test;", + "", + "import dagger.Module;", + "import dagger.hilt.testing.TestInstallIn;", + "", + "@Module", + "@TestInstallIn(components = {}, replaces = InstallInModule.class)", + "interface TestInstallInModule {}"); + Compilation compilation = compiler().compile(installInModule, testInstallInModule); + assertThat(compilation).failed(); + assertThat(compilation).hadErrorCount(1); + // TODO(bcorso): Add inFile().onLine() whenever we've fixed Processors.getAnnotationClassValues + assertThat(compilation) + .hadErrorContaining( + "@TestInstallIn, 'components' class is invalid or missing: " + + "@dagger.hilt.testing.TestInstallIn(" + + "components={}, replaces={test.InstallInModule.class})"); + } + + @Test + public void testEmptyReplacesValues() { + JavaFileObject testInstallInModule = + JavaFileObjects.forSourceLines( + "test.TestInstallInModule", + "package test;", + "", + "import dagger.Module;", + "import dagger.hilt.testing.TestInstallIn;", + "import dagger.hilt.components.SingletonComponent;", + "", + "@Module", + "@TestInstallIn(components = SingletonComponent.class, replaces = {})", + "interface TestInstallInModule {}"); + Compilation compilation = compiler().compile(testInstallInModule); + assertThat(compilation).failed(); + assertThat(compilation).hadErrorCount(1); + // TODO(bcorso): Add inFile().onLine() whenever we've fixed Processors.getAnnotationClassValues + assertThat(compilation) + .hadErrorContaining( + "@TestInstallIn, 'replaces' class is invalid or missing: " + + "@dagger.hilt.testing.TestInstallIn(" + + "components={dagger.hilt.components.SingletonComponent.class}, replaces={})"); + } + + @Test + public void testMissingModuleAnnotation() { + JavaFileObject installInModule = + JavaFileObjects.forSourceLines( + "test.InstallInModule", + "package test;", + "", + "import dagger.Module;", + "import dagger.hilt.InstallIn;", + "import dagger.hilt.components.SingletonComponent;", + "", + "@Module", + "@InstallIn(SingletonComponent.class)", + "interface InstallInModule {}"); + JavaFileObject testInstallInModule = + JavaFileObjects.forSourceLines( + "test.TestInstallInModule", + "package test;", + "", + "import dagger.Module;", + "import dagger.hilt.components.SingletonComponent;", + "import dagger.hilt.testing.TestInstallIn;", + "", + "@TestInstallIn(", + " components = SingletonComponent.class,", + " replaces = InstallInModule.class)", + "interface TestInstallInModule {}"); + Compilation compilation = compiler().compile(installInModule, testInstallInModule); + assertThat(compilation).failed(); + assertThat(compilation).hadErrorCount(1); + assertThat(compilation) + .hadErrorContaining( + "@TestInstallIn-annotated classes must also be annotated with @Module or @EntryPoint: " + + "test.TestInstallInModule") + .inFile(testInstallInModule) + .onLine(10); + } + + @Test + public void testInvalidUsageOnEntryPoint() { + JavaFileObject installInModule = + JavaFileObjects.forSourceLines( + "test.InstallInModule", + "package test;", + "", + "import dagger.Module;", + "import dagger.hilt.InstallIn;", + "import dagger.hilt.components.SingletonComponent;", + "", + "@Module", + "@InstallIn(SingletonComponent.class)", + "interface InstallInModule {}"); + JavaFileObject testInstallInEntryPoint = + JavaFileObjects.forSourceLines( + "test.TestInstallInEntryPoint", + "package test;", + "", + "import dagger.hilt.EntryPoint;", + "import dagger.hilt.components.SingletonComponent;", + "import dagger.hilt.testing.TestInstallIn;", + "", + "@EntryPoint", + "@TestInstallIn(", + " components = SingletonComponent.class,", + " replaces = InstallInModule.class)", + "interface TestInstallInEntryPoint {}"); + Compilation compilation = compiler().compile(installInModule, testInstallInEntryPoint); + assertThat(compilation).failed(); + assertThat(compilation).hadErrorCount(1); + assertThat(compilation) + .hadErrorContaining("@TestInstallIn can only be used with modules") + .inFile(testInstallInEntryPoint) + .onLine(11); + } + + @Test + public void testInvalidReplaceModules() { + JavaFileObject foo = + JavaFileObjects.forSourceLines("test.Foo", "package test;", "", "class Foo {}"); + JavaFileObject testInstallInModule = + JavaFileObjects.forSourceLines( + "test.TestInstallInModule", + "package test;", + "", + "import dagger.Module;", + "import dagger.hilt.components.SingletonComponent;", + "import dagger.hilt.testing.TestInstallIn;", + "", + "@Module", + "@TestInstallIn(", + " components = SingletonComponent.class,", + " replaces = Foo.class)", + "interface TestInstallInModule {}"); + Compilation compilation = compiler().compile(foo, testInstallInModule); + assertThat(compilation).failed(); + assertThat(compilation).hadErrorCount(1); + assertThat(compilation) + .hadErrorContaining( + "@TestInstallIn#replaces() can only contain @InstallIn modules, but found: [test.Foo]") + .inFile(testInstallInModule) + .onLine(11); + } + + @Test + public void testInternalDaggerReplaceModules() { + JavaFileObject testInstallInModule = + JavaFileObjects.forSourceLines( + "test.TestInstallInModule", + "package test;", + "", + "import dagger.Module;", + "import dagger.hilt.components.SingletonComponent;", + "import dagger.hilt.testing.TestInstallIn;", + "", + "@Module", + "@TestInstallIn(", + " components = SingletonComponent.class,", + " replaces = dagger.hilt.android.internal.modules.ApplicationContextModule.class)", + "interface TestInstallInModule {}"); + Compilation compilation = compiler().compile(testInstallInModule); + assertThat(compilation).failed(); + assertThat(compilation).hadErrorCount(1); + assertThat(compilation) + .hadErrorContaining( + "@TestInstallIn#replaces() cannot contain internal Hilt modules, but found: " + + "[dagger.hilt.android.internal.modules.ApplicationContextModule]") + .inFile(testInstallInModule) + .onLine(11); + } + + @Test + public void testHiltWrapperDaggerReplaceModules() { + JavaFileObject testInstallInModule = + JavaFileObjects.forSourceLines( + "test.TestInstallInModule", + "package test;", + "", + "import dagger.Module;", + "import dagger.hilt.components.SingletonComponent;", + "import dagger.hilt.testing.TestInstallIn;", + "import" + + " dagger.hilt.android.processor.internal.aggregateddeps.HiltWrapper_InstallInModule;", + "", + "@Module", + "@TestInstallIn(", + " components = SingletonComponent.class,", + // Note: this module is built in a separate library since AggregatedDepsProcessor can't + // handle modules generated in the same round. + " replaces = HiltWrapper_InstallInModule.class)", + "interface TestInstallInModule {}"); + Compilation compilation = compiler().compile(testInstallInModule); + assertThat(compilation).failed(); + assertThat(compilation).hadErrorCount(1); + assertThat(compilation) + .hadErrorContaining( + "@TestInstallIn#replaces() cannot contain Hilt generated public wrapper modules, " + + "but found: [dagger.hilt.android.processor.internal.aggregateddeps." + + "HiltWrapper_InstallInModule]") + .inFile(testInstallInModule) + .onLine(12); + } + + @Test + public void testCannotReplaceLocalInstallInModule() { + JavaFileObject test = + JavaFileObjects.forSourceLines( + "test.MyTest", + "package test;", + "", + "import dagger.Module;", + "import dagger.hilt.InstallIn;", + "import dagger.hilt.components.SingletonComponent;", + "import dagger.hilt.testing.TestInstallIn;", + "import dagger.hilt.android.testing.HiltAndroidTest;", + "", + "@HiltAndroidTest", + "public class MyTest {", + " @Module", + " @InstallIn(SingletonComponent.class)", + " interface LocalInstallInModule {}", + "}"); + JavaFileObject testInstallIn = + JavaFileObjects.forSourceLines( + "test.TestInstallInModule", + "package test;", + "", + "import dagger.Module;", + "import dagger.hilt.components.SingletonComponent;", + "import dagger.hilt.testing.TestInstallIn;", + "", + "@Module", + "@TestInstallIn(", + " components = SingletonComponent.class,", + " replaces = MyTest.LocalInstallInModule.class)", + "interface TestInstallInModule {}"); + Compilation compilation = compiler().compile(test, testInstallIn); + assertThat(compilation).failed(); + assertThat(compilation).hadErrorCount(1); + assertThat(compilation) + .hadErrorContaining( + "TestInstallIn#replaces() cannot replace test specific @InstallIn modules, but found: " + + "[test.MyTest.LocalInstallInModule].") + .inFile(testInstallIn) + .onLine(11); + } + + @Test + public void testThatTestInstallInCannotOriginateFromTest() { + JavaFileObject installInModule = + JavaFileObjects.forSourceLines( + "test.InstallInModule", + "package test;", + "", + "import dagger.Module;", + "import dagger.hilt.InstallIn;", + "import dagger.hilt.components.SingletonComponent;", + "", + "@Module", + "@InstallIn(SingletonComponent.class)", + "interface InstallInModule {}"); + JavaFileObject test = + JavaFileObjects.forSourceLines( + "test.MyTest", + "package test;", + "", + "import dagger.Module;", + "import dagger.hilt.components.SingletonComponent;", + "import dagger.hilt.testing.TestInstallIn;", + "import dagger.hilt.android.testing.HiltAndroidTest;", + "", + "@HiltAndroidTest", + "public class MyTest {", + " @Module", + " @TestInstallIn(", + " components = SingletonComponent.class,", + " replaces = InstallInModule.class)", + " interface TestInstallInModule {}", + "}"); + Compilation compilation = compiler().compile(test, installInModule); + assertThat(compilation).failed(); + assertThat(compilation).hadErrorCount(1); + assertThat(compilation) + .hadErrorContaining( + "@TestInstallIn modules cannot be nested in (or originate from) a " + + "@HiltAndroidTest-annotated class: test.MyTest") + .inFile(test) + .onLine(14); + } +}