/* * 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.collect.Iterables.getOnlyElement; import static dagger.hilt.processor.internal.HiltCompilerOptions.isModuleInstallInCheckDisabled; import static dagger.internal.codegen.extension.DaggerStreams.toImmutableList; import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet; import androidx.room.compiler.processing.XAnnotation; import androidx.room.compiler.processing.XElement; import androidx.room.compiler.processing.XElementKt; import androidx.room.compiler.processing.XExecutableElement; import androidx.room.compiler.processing.XMethodElement; import androidx.room.compiler.processing.XProcessingEnv; import androidx.room.compiler.processing.XTypeElement; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.squareup.javapoet.ClassName; import dagger.hilt.processor.internal.BaseProcessingStep; import dagger.hilt.processor.internal.ClassNames; import dagger.hilt.processor.internal.Components; import dagger.hilt.processor.internal.ProcessorErrors; import dagger.hilt.processor.internal.Processors; import dagger.internal.codegen.extension.DaggerStreams; import dagger.internal.codegen.xprocessing.XElements; import java.util.HashSet; import java.util.Optional; import java.util.Set; /** Processor that outputs dummy files to propagate information through multiple javac runs. */ public final class AggregatedDepsProcessingStep extends BaseProcessingStep { private static final ImmutableSet ENTRY_POINT_ANNOTATIONS = ImmutableSet.of( ClassNames.ENTRY_POINT, ClassNames.EARLY_ENTRY_POINT, 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<>(); public AggregatedDepsProcessingStep(XProcessingEnv env) { super(env); } @Override protected ImmutableSet annotationClassNames() { return ImmutableSet.builder() .addAll(INSTALL_IN_ANNOTATIONS) .addAll(MODULE_ANNOTATIONS) .addAll(ENTRY_POINT_ANNOTATIONS) .build(); } @Override public void processEach(ClassName annotation, XElement element) throws Exception { if (!seen.add(element)) { return; } 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(); ProcessorErrors.checkState( !hasInstallIn || isEntryPoint || isModule, element, "@%s-annotated classes must also be annotated with @Module or @EntryPoint: %s", installInAnnotation.map(ClassName::simpleName).orElse("@InstallIn"), XElements.toStableString(element)); ProcessorErrors.checkState( !(isEntryPoint && isModule), element, "@%s and @%s cannot be used on the same interface: %s", moduleAnnotation.map(ClassName::simpleName).orElse("@Module"), entryPointAnnotation.map(ClassName::simpleName).orElse("@EntryPoint"), XElements.toStableString(element)); if (isModule) { processModule(element, installInAnnotation, moduleAnnotation.get()); } else if (isEntryPoint) { processEntryPoint(element, installInAnnotation, entryPointAnnotation.get()); } else { throw new AssertionError(); } } private void processModule( XElement element, Optional installInAnnotation, ClassName moduleAnnotation) throws Exception { ProcessorErrors.checkState( installInAnnotation.isPresent() || isDaggerGeneratedModule(element) || installInCheckDisabled(element), element, "%s is missing an @InstallIn annotation. If this was intentional, see" + " https://dagger.dev/hilt/flags#disable-install-in-check for how to disable this" + " check.", XElements.toStableString(element)); if (!installInAnnotation.isPresent()) { // Modules without @InstallIn or @TestInstallIn annotations don't need to be processed further return; } ProcessorErrors.checkState( XElementKt.isTypeElement(element), element, "Only classes and interfaces can be annotated with @Module: %s", XElements.toStableString(element)); XTypeElement module = XElements.asTypeElement(element); ProcessorErrors.checkState( module.isClass() || module.isInterface() || module.isKotlinObject(), module, "Only classes and interfaces can be annotated with @Module: %s", XElements.toStableString(module)); ProcessorErrors.checkState( Processors.isTopLevel(module) || module.isStatic() || module.isAbstract() || module.getEnclosingElement().hasAnnotation(ClassNames.HILT_ANDROID_TEST), module, "Nested @%s modules must be static unless they are directly nested within a test. " + "Found: %s", installInAnnotation.get().simpleName(), XElements.toStableString(module)); // Check that if Dagger needs an instance of the module, Hilt can provide it automatically by // calling a visible empty constructor. ProcessorErrors.checkState( // Skip ApplicationContextModule, since Hilt manages this module internally. ClassNames.APPLICATION_CONTEXT_MODULE.equals(module.getClassName()) || !Processors.requiresModuleInstance(module) || Processors.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 = module.getDeclaredMethods().stream() .filter(XMethodElement::isAbstract) .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.stream() .map(XElements::toStableString) .collect(DaggerStreams.toImmutableList()), XElements.toStableString(module)); ImmutableList replacedModules = ImmutableList.of(); if (module.hasAnnotation(ClassNames.TEST_INSTALL_IN)) { Optional originatingTestElement = Processors.getOriginatingTestElement(module); ProcessorErrors.checkState( !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(XTypeElement::getQualifiedName).orElse("")); XAnnotation testInstallIn = module.getAnnotation(ClassNames.TEST_INSTALL_IN); replacedModules = Processors.getAnnotationClassValues(testInstallIn, "replaces"); ProcessorErrors.checkState( !replacedModules.isEmpty(), // TODO(b/152801981): this should really error on the annotation value module, "@TestInstallIn#replaces() cannot be empty. Use @InstallIn instead."); ImmutableList nonInstallInModules = replacedModules.stream() .filter(replacedModule -> !replacedModule.hasAnnotation(ClassNames.INSTALL_IN)) .collect(toImmutableList()); ProcessorErrors.checkState( nonInstallInModules.isEmpty(), // TODO(b/152801981): this should really error on the annotation value module, "@TestInstallIn#replaces() can only contain @InstallIn modules, but found: %s", nonInstallInModules.stream() .map(XElements::toStableString) .collect(DaggerStreams.toImmutableList())); ImmutableList hiltWrapperModules = replacedModules.stream() .filter( replacedModule -> replacedModule.getClassName().simpleName().startsWith("HiltWrapper_")) .collect(toImmutableList()); ProcessorErrors.checkState( hiltWrapperModules.isEmpty(), // TODO(b/152801981): this should really error on the annotation value module, "@TestInstallIn#replaces() cannot contain Hilt generated public wrapper modules, " + "but found: %s. ", hiltWrapperModules.stream() .map(XElements::toStableString) .collect(DaggerStreams.toImmutableList())); if (!module.getPackageName().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 -> replacedModule.getPackageName().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.stream() .map(XElements::toStableString) .collect(DaggerStreams.toImmutableList())); } // Prevent users from uninstalling test-specific @InstallIn modules. ImmutableList replacedTestSpecificInstallIn = replacedModules.stream() .filter( replacedModule -> Processors.getOriginatingTestElement(replacedModule).isPresent()) .collect(toImmutableList()); ProcessorErrors.checkState( 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.stream() .map(XElements::toStableString) .collect(DaggerStreams.toImmutableList())); } generateAggregatedDeps( "modules", module, moduleAnnotation, replacedModules.stream().map(XTypeElement::getClassName).collect(toImmutableSet())); } private void processEntryPoint( XElement element, Optional installInAnnotation, ClassName entryPointAnnotation) throws Exception { ProcessorErrors.checkState( installInAnnotation.isPresent() , element, "@%s %s must also be annotated with @InstallIn", entryPointAnnotation.simpleName(), XElements.toStableString(element)); ProcessorErrors.checkState( !element.hasAnnotation(ClassNames.TEST_INSTALL_IN), element, "@TestInstallIn can only be used with modules"); ProcessorErrors.checkState( XElementKt.isTypeElement(element) && XElements.asTypeElement(element).isInterface(), element, "Only interfaces can be annotated with @%s: %s", entryPointAnnotation.simpleName(), XElements.toStableString(element)); XTypeElement entryPoint = XElements.asTypeElement(element); if (entryPointAnnotation.equals(ClassNames.EARLY_ENTRY_POINT)) { ImmutableSet components = Components.getComponents(element); ProcessorErrors.checkState( components.equals(ImmutableSet.of(ClassNames.SINGLETON_COMPONENT)), element, "@EarlyEntryPoint can only be installed into the SingletonComponent. Found: %s", components); Optional optionalTestElement = Processors.getOriginatingTestElement(element); ProcessorErrors.checkState( !optionalTestElement.isPresent(), element, "@EarlyEntryPoint-annotated entry point, %s, cannot be nested in (or originate from) " + "a @HiltAndroidTest-annotated class, %s. This requirement is to avoid confusion " + "with other, test-specific entry points.", entryPoint.getQualifiedName(), optionalTestElement.map(testElement -> testElement.getQualifiedName()).orElse("")); } generateAggregatedDeps( entryPointAnnotation.equals(ClassNames.COMPONENT_ENTRY_POINT) ? "componentEntryPoints" : "entryPoints", entryPoint, entryPointAnnotation, ImmutableSet.of()); } private void generateAggregatedDeps( String key, XTypeElement 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(element); if (isValidKind(element)) { Optional pkgPrivateMetadata = PkgPrivateMetadata.of(element, annotation); if (pkgPrivateMetadata.isPresent()) { if (key.contentEquals("modules")) { new PkgPrivateModuleGenerator(processingEnv(), pkgPrivateMetadata.get()).generate(); } else { new PkgPrivateEntryPointGenerator(processingEnv(), pkgPrivateMetadata.get()).generate(); } } else { Optional testName = Processors.getOriginatingTestElement(element).map(XTypeElement::getClassName); new AggregatedDepsGenerator(key, element, testName, components, replacedModules).generate(); } } } private static Optional getAnnotation( XElement element, ImmutableSet annotations) { ImmutableSet usedAnnotations = annotations.stream().filter(element::hasAnnotation).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", XElements.toStableString(element), usedAnnotations); return Optional.of(getOnlyElement(usedAnnotations)); } private static boolean isValidKind(XElement 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. return !XElements.asTypeElement(element).getType().isError(); } private boolean installInCheckDisabled(XElement element) { return isModuleInstallInCheckDisabled(processingEnv()) || element.hasAnnotation(ClassNames.DISABLE_INSTALL_IN_CHECK); } /** * When using Dagger Producers, don't process generated modules. They will not have the expected * annotations. */ private static boolean isDaggerGeneratedModule(XElement element) { if (!element.hasAnnotation(ClassNames.MODULE)) { return false; } return element.getAllAnnotations().stream() .filter(annotation -> isGenerated(annotation)) .map(annotation -> getOnlyElement(annotation.getAsStringList("value"))) .anyMatch(value -> value.startsWith("dagger")); } private static boolean isGenerated(XAnnotation annotation) { String name = annotation.getTypeElement().getQualifiedName(); return name.equals("javax.annotation.Generated") || name.equals("javax.annotation.processing.Generated"); } }