/* * 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.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; import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet; import static javax.lang.model.element.ElementKind.CLASS; import static javax.lang.model.element.ElementKind.INTERFACE; import static javax.lang.model.element.Modifier.ABSTRACT; import static javax.lang.model.element.Modifier.PRIVATE; import static javax.lang.model.element.Modifier.STATIC; import static net.ltgt.gradle.incap.IncrementalAnnotationProcessorType.ISOLATING; import com.google.auto.service.AutoService; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.squareup.javapoet.ClassName; import dagger.hilt.processor.internal.BaseProcessor; import dagger.hilt.processor.internal.ClassNames; import dagger.hilt.processor.internal.Components; import dagger.hilt.processor.internal.KotlinMetadataUtils; import dagger.hilt.processor.internal.ProcessorErrors; import dagger.hilt.processor.internal.Processors; import dagger.internal.codegen.kotlin.KotlinMetadataUtil; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import javax.annotation.processing.Processor; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Name; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeKind; import javax.lang.model.util.ElementFilter; import javax.lang.model.util.SimpleAnnotationValueVisitor8; import net.ltgt.gradle.incap.IncrementalAnnotationProcessor; /** Processor that outputs dummy files to propagate information through multiple javac runs. */ @IncrementalAnnotationProcessor(ISOLATING) @AutoService(Processor.class) public final class AggregatedDepsProcessor extends BaseProcessor { private static final ImmutableSet ENTRY_POINT_ANNOTATIONS = ImmutableSet.of( ClassNames.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<>(); @Override public Set getSupportedAnnotationTypes() { return ImmutableSet.builder() .addAll(INSTALL_IN_ANNOTATIONS) .addAll(MODULE_ANNOTATIONS) .addAll(ENTRY_POINT_ANNOTATIONS) .build() .stream() .map(Object::toString) .collect(toImmutableSet()); } @Override public void processEach(TypeElement annotation, Element 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"), 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"), 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( 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.", 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); 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( !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( !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 -> !Processors.hasAnnotation(replacedModule, 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); ImmutableList hiltWrapperModules = replacedModules.stream() .filter( replacedModule -> replacedModule.getSimpleName().toString().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); 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); } // Prevent users from uninstalling test-specific @InstallIn modules. ImmutableList replacedTestSpecificInstallIn = replacedModules.stream() .filter(replacedModule -> 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); } 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 { 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. return element.asType().getKind() != TypeKind.ERROR; } private boolean installInCheckDisabled(Element element) { return DISABLE_MODULES_HAVE_INSTALL_IN_CHECK.get(getProcessingEnv()) || Processors.hasAnnotation(element, 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(Element element) { if (!Processors.hasAnnotation(element, ClassNames.MODULE)) { return false; } return element.getAnnotationMirrors().stream() .filter(mirror -> isGenerated(mirror)) .map(mirror -> asString(getOnlyElement(asList(getAnnotationValue(mirror, "value"))))) .anyMatch(value -> value.startsWith("dagger")); } private static List asList(AnnotationValue value) { return value.accept( new SimpleAnnotationValueVisitor8, Void>() { @Override public List visitArray( List value, Void unused) { return value; } }, null); } private static String asString(AnnotationValue value) { return value.accept( new SimpleAnnotationValueVisitor8() { @Override public String visitString(String value, Void unused) { return value; } }, null); } private static boolean isGenerated(AnnotationMirror annotationMirror) { Name name = asType(annotationMirror.getAnnotationType().asElement()).getQualifiedName(); return name.contentEquals("javax.annotation.Generated") || name.contentEquals("javax.annotation.processing.Generated"); } private static boolean daggerRequiresModuleInstance(TypeElement module) { return !module.getModifiers().contains(ABSTRACT) && !hasOnlyStaticProvides(module) // Skip ApplicationContextModule, since Hilt manages this module internally. && !ClassNames.APPLICATION_CONTEXT_MODULE.equals(ClassName.get(module)) // Skip Kotlin object modules since all their provision methods are static && !isKotlinObject(module); } private static boolean isKotlinObject(TypeElement type) { KotlinMetadataUtil metadataUtil = KotlinMetadataUtils.getMetadataUtil(); return metadataUtil.isObjectClass(type) || metadataUtil.isCompanionObjectClass(type); } private static boolean hasOnlyStaticProvides(TypeElement module) { // TODO(erichang): Check for @Produces too when we have a producers story return ElementFilter.methodsIn(module.getEnclosedElements()).stream() .filter(method -> Processors.hasAnnotation(method, ClassNames.PROVIDES)) .allMatch(method -> method.getModifiers().contains(STATIC)); } private static boolean hasVisibleEmptyConstructor(TypeElement type) { List constructors = ElementFilter.constructorsIn(type.getEnclosedElements()); return constructors.isEmpty() || constructors.stream() .filter(constructor -> constructor.getParameters().isEmpty()) .anyMatch( constructor -> !constructor.getModifiers().contains(PRIVATE) ); } }