blob: 151401e6082bea0f537d29b1eb99f44eee3570f2 [file] [log] [blame]
/*
* 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.processor.internal.HiltCompilerOptions.isModuleInstallInCheckDisabled;
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.ProcessorErrors;
import dagger.hilt.processor.internal.Processors;
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.EARLY_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(
// Skip ApplicationContextModule, since Hilt manages this module internally.
ClassNames.APPLICATION_CONTEXT_MODULE.equals(ClassName.get(module))
|| !Processors.requiresModuleInstance(getElementUtils(), 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 =
Processors.getOriginatingTestElement(module, getElementUtils());
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 ->
Processors.getOriginatingTestElement(replacedModule, getElementUtils())
.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);
if (entryPointAnnotation.equals(ClassNames.EARLY_ENTRY_POINT)) {
ImmutableSet<ClassName> components = Components.getComponents(getElementUtils(), element);
ProcessorErrors.checkState(
components.equals(ImmutableSet.of(ClassNames.SINGLETON_COMPONENT)),
element,
"@EarlyEntryPoint can only be installed into the SingletonComponent. Found: %s",
components);
Optional<TypeElement> optionalTestElement =
Processors.getOriginatingTestElement(element, getElementUtils());
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.",
asType(element).getQualifiedName().toString(),
optionalTestElement
.map(testElement -> testElement.getQualifiedName().toString())
.orElse(""));
}
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 =
Processors.getOriginatingTestElement(element, getElementUtils()).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 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 isModuleInstallInCheckDisabled(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 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)
);
}
}