| /* |
| * 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<ClassName> ENTRY_POINT_ANNOTATIONS = |
| ImmutableSet.of( |
| ClassNames.ENTRY_POINT, |
| ClassNames.GENERATED_ENTRY_POINT, |
| ClassNames.COMPONENT_ENTRY_POINT); |
| |
| private static final ImmutableSet<ClassName> MODULE_ANNOTATIONS = |
| ImmutableSet.of( |
| ClassNames.MODULE); |
| |
| private static final ImmutableSet<ClassName> INSTALL_IN_ANNOTATIONS = |
| ImmutableSet.of(ClassNames.INSTALL_IN, ClassNames.TEST_INSTALL_IN); |
| |
| private final Set<Element> seen = new HashSet<>(); |
| |
| @Override |
| public Set<String> 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<ClassName> installInAnnotation = getAnnotation(element, INSTALL_IN_ANNOTATIONS); |
| Optional<ClassName> entryPointAnnotation = getAnnotation(element, ENTRY_POINT_ANNOTATIONS); |
| Optional<ClassName> 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<ClassName> 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<ExecutableElement> 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<TypeElement> replacedModules = ImmutableList.of(); |
| if (Processors.hasAnnotation(module, ClassNames.TEST_INSTALL_IN)) { |
| Optional<TypeElement> 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<TypeElement> 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<TypeElement> 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<TypeElement> 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<TypeElement> 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<ClassName> 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<ClassName> replacedModules) |
| throws Exception { |
| // Get @InstallIn components here to catch errors before skipping user's pkg-private element. |
| ImmutableSet<ClassName> components = Components.getComponents(getElementUtils(), element); |
| |
| if (isValidKind(element)) { |
| Optional<PkgPrivateMetadata> 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<ClassName> testName = getOriginatingTestElement(element).map(ClassName::get); |
| new AggregatedDepsGenerator( |
| key, element, testName, components, replacedModules, getProcessingEnv()) |
| .generate(); |
| } |
| } |
| } |
| |
| private static Optional<ClassName> getAnnotation( |
| Element element, ImmutableSet<ClassName> annotations) { |
| ImmutableSet<ClassName> 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<TypeElement> 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<? extends AnnotationValue> asList(AnnotationValue value) { |
| return value.accept( |
| new SimpleAnnotationValueVisitor8<List<? extends AnnotationValue>, Void>() { |
| @Override |
| public List<? extends AnnotationValue> visitArray( |
| List<? extends AnnotationValue> value, Void unused) { |
| return value; |
| } |
| }, |
| null); |
| } |
| |
| private static String asString(AnnotationValue value) { |
| return value.accept( |
| new SimpleAnnotationValueVisitor8<String, Void>() { |
| @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<ExecutableElement> constructors = ElementFilter.constructorsIn(type.getEnclosedElements()); |
| return constructors.isEmpty() |
| || constructors.stream() |
| .filter(constructor -> constructor.getParameters().isEmpty()) |
| .anyMatch( |
| constructor -> |
| !constructor.getModifiers().contains(PRIVATE) |
| ); |
| } |
| } |