blob: e5868f8619bc3ec1fdd073cfef5a9a83156a3ece [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.android.processor.internal.androidentrypoint;
import static dagger.hilt.android.processor.internal.androidentrypoint.HiltCompilerOptions.BooleanOption.DISABLE_ANDROID_SUPERCLASS_VALIDATION;
import static dagger.internal.codegen.extension.DaggerStreams.toImmutableSet;
import com.google.auto.common.MoreElements;
import com.google.auto.common.MoreTypes;
import com.google.auto.value.AutoValue;
import com.google.auto.value.extension.memoized.Memoized;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.TypeName;
import dagger.hilt.android.processor.internal.AndroidClassNames;
import dagger.hilt.processor.internal.BadInputException;
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.LinkedHashSet;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
/** Metadata class for @AndroidEntryPoint annotated classes. */
@AutoValue
public abstract class AndroidEntryPointMetadata {
/** The class {@link Element} annotated with @AndroidEntryPoint. */
public abstract TypeElement element();
/** The base class {@link Element} given to @AndroidEntryPoint. */
public abstract TypeElement baseElement();
/** The name of the generated base class, beginning with 'Hilt_'. */
public abstract ClassName generatedClassName();
/** Returns {@code true} if the class requires bytecode injection to replace the base class. */
public abstract boolean requiresBytecodeInjection();
/** Returns the {@link AndroidType} for the annotated element. */
public abstract AndroidType androidType();
/** Returns {@link Optional} of {@link AndroidEntryPointMetadata}. */
public abstract Optional<AndroidEntryPointMetadata> baseMetadata();
/** Returns set of scopes that the component interface should be installed in. */
public abstract ImmutableSet<ClassName> installInComponents();
/** Returns the component manager this generated Hilt class should use. */
public abstract TypeName componentManager();
/** Returns the initialization arguments for the component manager. */
public abstract Optional<CodeBlock> componentManagerInitArgs();
/**
* Returns the metadata for the root most class in the hierarchy.
*
* <p>If this is the only metadata in the class hierarchy, it returns this.
*/
@Memoized
public AndroidEntryPointMetadata rootMetadata() {
return baseMetadata().map(AndroidEntryPointMetadata::rootMetadata).orElse(this);
}
boolean isRootMetadata() {
return this.equals(rootMetadata());
}
/** Returns true if this class allows optional injection. */
public boolean allowsOptionalInjection() {
return Processors.hasAnnotation(element(), AndroidClassNames.OPTIONAL_INJECT);
}
/** Returns true if any base class (transitively) allows optional injection. */
public boolean baseAllowsOptionalInjection() {
return baseMetadata().isPresent() && baseMetadata().get().allowsOptionalInjection();
}
/** Returns true if any base class (transitively) uses @AndroidEntryPoint. */
public boolean overridesAndroidEntryPointClass() {
return baseMetadata().isPresent();
}
/** The name of the class annotated with @AndroidEntryPoint */
public ClassName elementClassName() {
return ClassName.get(element());
}
/** The name of the base class given to @AndroidEntryPoint */
public TypeName baseClassName() {
return TypeName.get(baseElement().asType());
}
/** The name of the generated injector for the Hilt class. */
public ClassName injectorClassName() {
return Processors.append(
Processors.getEnclosedClassName(elementClassName()), "_GeneratedInjector");
}
/**
* The name of inject method for this class. The format is: inject$CLASS. If the class is nested,
* will return the full name deliminated with '_'. e.g. Foo.Bar.Baz -> injectFoo_Bar_Baz
*/
public String injectMethodName() {
return "inject" + Processors.getEnclosedName(elementClassName());
}
/** Returns the @InstallIn annotation for the module providing this class. */
public final AnnotationSpec injectorInstallInAnnotation() {
return Components.getInstallInAnnotationSpec(installInComponents());
}
public ParameterSpec componentManagerParam() {
return ParameterSpec.builder(componentManager(), "componentManager").build();
}
/**
* Modifiers that should be applied to the generated class.
*
* <p>Note that the generated class must have public visibility if used by a
* public @AndroidEntryPoint-annotated kotlin class. See:
* https://discuss.kotlinlang.org/t/why-does-kotlin-prohibit-exposing-restricted-visibility-types/7047
*/
public Modifier[] generatedClassModifiers() {
return isKotlinClass(element()) && element().getModifiers().contains(Modifier.PUBLIC)
? new Modifier[] {Modifier.ABSTRACT, Modifier.PUBLIC}
: new Modifier[] {Modifier.ABSTRACT};
}
private static ClassName generatedClassName(TypeElement element) {
String prefix = "Hilt_";
return Processors.prepend(Processors.getEnclosedClassName(ClassName.get(element)), prefix);
}
private static final ImmutableSet<ClassName> HILT_ANNOTATION_NAMES =
ImmutableSet.of(
AndroidClassNames.HILT_ANDROID_APP,
AndroidClassNames.ANDROID_ENTRY_POINT);
private static ImmutableSet<? extends AnnotationMirror> hiltAnnotations(Element element) {
return element.getAnnotationMirrors().stream()
.filter(mirror -> HILT_ANNOTATION_NAMES.contains(ClassName.get(mirror.getAnnotationType())))
.collect(toImmutableSet());
}
/** Returns true if the given element has Android Entry Point metadata. */
public static boolean hasAndroidEntryPointMetadata(Element element) {
return !hiltAnnotations(element).isEmpty();
}
/** Returns the {@link AndroidEntryPointMetadata} for a @AndroidEntryPoint annotated element. */
public static AndroidEntryPointMetadata of(ProcessingEnvironment env, Element element) {
LinkedHashSet<Element> inheritanceTrace = new LinkedHashSet<>();
inheritanceTrace.add(element);
return of(env, element, inheritanceTrace);
}
public static AndroidEntryPointMetadata manuallyConstruct(
TypeElement element,
TypeElement baseElement,
ClassName generatedClassName,
boolean requiresBytecodeInjection,
AndroidType androidType,
Optional<AndroidEntryPointMetadata> baseMetadata,
ImmutableSet<ClassName> installInComponents,
TypeName componentManager,
Optional<CodeBlock> componentManagerInitArgs) {
return new AutoValue_AndroidEntryPointMetadata(
element,
baseElement,
generatedClassName,
requiresBytecodeInjection,
androidType,
baseMetadata,
installInComponents,
componentManager,
componentManagerInitArgs);
}
/**
* Internal implementation for "of" method, checking inheritance cycle utilizing inheritanceTrace
* along the way.
*/
private static AndroidEntryPointMetadata of(
ProcessingEnvironment env, Element element, LinkedHashSet<Element> inheritanceTrace) {
ImmutableSet<? extends AnnotationMirror> hiltAnnotations = hiltAnnotations(element);
ProcessorErrors.checkState(
hiltAnnotations.size() == 1,
element,
"Expected exactly 1 of %s. Found: %s",
HILT_ANNOTATION_NAMES,
hiltAnnotations);
ClassName annotationClassName =
ClassName.get(
MoreTypes.asTypeElement(Iterables.getOnlyElement(hiltAnnotations).getAnnotationType()));
ProcessorErrors.checkState(
element.getKind() == ElementKind.CLASS,
element,
"Only classes can be annotated with @%s",
annotationClassName.simpleName());
TypeElement androidEntryPointElement = MoreElements.asType(element);
ProcessorErrors.checkState(
androidEntryPointElement.getTypeParameters().isEmpty(),
element,
"@%s-annotated classes cannot have type parameters.",
annotationClassName.simpleName());
final TypeElement androidEntryPointClassValue =
Processors.getAnnotationClassValue(
env.getElementUtils(),
Processors.getAnnotationMirror(androidEntryPointElement, annotationClassName),
"value");
final TypeElement baseElement;
final ClassName generatedClassName;
boolean requiresBytecodeInjection =
DISABLE_ANDROID_SUPERCLASS_VALIDATION.get(env)
&& MoreTypes.isTypeOf(Void.class, androidEntryPointClassValue.asType());
if (requiresBytecodeInjection) {
baseElement = MoreElements.asType(env.getTypeUtils().asElement(androidEntryPointElement.getSuperclass()));
// If this AndroidEntryPoint is a Kotlin class and its base type is also Kotlin and has
// default values declared in its constructor then error out because for the short-form
// usage of @AndroidEntryPoint the bytecode transformation will be done incorrectly.
KotlinMetadataUtil metadataUtil = KotlinMetadataUtils.getMetadataUtil();
ProcessorErrors.checkState(
!metadataUtil.hasMetadata(androidEntryPointElement)
|| !metadataUtil.containsConstructorWithDefaultParam(baseElement),
baseElement,
"The base class, '%s', of the @AndroidEntryPoint, '%s', contains a constructor with "
+ "default parameters. This is currently not supported by the Gradle plugin. Either "
+ "specify the base class as described at "
+ "https://dagger.dev/hilt/gradle-setup#why-use-the-plugin or remove the default value "
+ "declaration.",
baseElement.getQualifiedName(),
androidEntryPointElement.getQualifiedName());
generatedClassName = generatedClassName(androidEntryPointElement);
} else {
baseElement = androidEntryPointClassValue;
ProcessorErrors.checkState(
!MoreTypes.isTypeOf(Void.class, baseElement.asType()),
androidEntryPointElement,
"Expected @%s to have a value."
+ " Did you forget to apply the Gradle Plugin? (dagger.hilt.android.plugin)\n"
+ "See https://dagger.dev/hilt/gradle-setup.html" ,
annotationClassName.simpleName());
// Check that the root $CLASS extends Hilt_$CLASS
String extendsName =
env.getTypeUtils()
.asElement(androidEntryPointElement.getSuperclass())
.getSimpleName()
.toString();
generatedClassName = generatedClassName(androidEntryPointElement);
ProcessorErrors.checkState(
extendsName.contentEquals(generatedClassName.simpleName()),
androidEntryPointElement,
"@%s class expected to extend %s. Found: %s",
annotationClassName.simpleName(),
generatedClassName.simpleName(),
extendsName);
}
Optional<AndroidEntryPointMetadata> baseMetadata =
baseMetadata(env, androidEntryPointElement, baseElement, inheritanceTrace);
if (baseMetadata.isPresent()) {
return manuallyConstruct(
androidEntryPointElement,
baseElement,
generatedClassName,
requiresBytecodeInjection,
baseMetadata.get().androidType(),
baseMetadata,
baseMetadata.get().installInComponents(),
baseMetadata.get().componentManager(),
baseMetadata.get().componentManagerInitArgs());
} else {
Type type = Type.of(androidEntryPointElement, baseElement);
return manuallyConstruct(
androidEntryPointElement,
baseElement,
generatedClassName,
requiresBytecodeInjection,
type.androidType,
Optional.empty(),
ImmutableSet.of(type.component),
type.manager,
Optional.ofNullable(type.componentManagerInitArgs));
}
}
private static Optional<AndroidEntryPointMetadata> baseMetadata(
ProcessingEnvironment env,
TypeElement element,
TypeElement baseElement,
LinkedHashSet<Element> inheritanceTrace) {
ProcessorErrors.checkState(
inheritanceTrace.add(baseElement),
element,
cyclicInheritanceErrorMessage(inheritanceTrace, baseElement));
if (hasAndroidEntryPointMetadata(baseElement)) {
AndroidEntryPointMetadata baseMetadata =
AndroidEntryPointMetadata.of(env, baseElement, inheritanceTrace);
checkConsistentAnnotations(element, baseMetadata);
return Optional.of(baseMetadata);
}
TypeMirror superClass = baseElement.getSuperclass();
// None type is returned if this is an interface or Object
if (superClass.getKind() != TypeKind.NONE && superClass.getKind() != TypeKind.ERROR) {
Preconditions.checkState(superClass.getKind() == TypeKind.DECLARED);
return baseMetadata(env, element, MoreTypes.asTypeElement(superClass), inheritanceTrace);
}
return Optional.empty();
}
private static String cyclicInheritanceErrorMessage(
LinkedHashSet<Element> inheritanceTrace, TypeElement cycleEntryPoint) {
return String.format(
"Cyclic inheritance detected. Make sure the base class of @AndroidEntryPoint "
+ "is not the annotated class itself or subclass of the annotated class.\n"
+ "The cyclic inheritance structure: %s --> %s\n",
inheritanceTrace.stream()
.map(Element::asType)
.map(TypeMirror::toString)
.collect(Collectors.joining(" --> ")),
cycleEntryPoint.asType());
}
private static boolean isKotlinClass(TypeElement typeElement) {
return typeElement.getAnnotationMirrors().stream()
.map(mirror -> mirror.getAnnotationType())
.anyMatch(type -> ClassName.get(type).equals(ClassNames.KOTLIN_METADATA));
}
/**
* The Android type of the Android Entry Point element. Component splits (like with fragment
* bindings) are coalesced.
*/
public enum AndroidType {
APPLICATION,
ACTIVITY,
BROADCAST_RECEIVER,
FRAGMENT,
SERVICE,
VIEW
}
/** The type of Android Entry Point element. This includes splits for different components. */
private static final class Type {
private static final Type APPLICATION =
new Type(
AndroidClassNames.SINGLETON_COMPONENT,
AndroidType.APPLICATION,
AndroidClassNames.APPLICATION_COMPONENT_MANAGER,
null);
private static final Type SERVICE =
new Type(
AndroidClassNames.SERVICE_COMPONENT,
AndroidType.SERVICE,
AndroidClassNames.SERVICE_COMPONENT_MANAGER,
CodeBlock.of("this"));
private static final Type BROADCAST_RECEIVER =
new Type(
AndroidClassNames.SINGLETON_COMPONENT,
AndroidType.BROADCAST_RECEIVER,
AndroidClassNames.BROADCAST_RECEIVER_COMPONENT_MANAGER,
null);
private static final Type ACTIVITY =
new Type(
AndroidClassNames.ACTIVITY_COMPONENT,
AndroidType.ACTIVITY,
AndroidClassNames.ACTIVITY_COMPONENT_MANAGER,
CodeBlock.of("this"));
private static final Type FRAGMENT =
new Type(
AndroidClassNames.FRAGMENT_COMPONENT,
AndroidType.FRAGMENT,
AndroidClassNames.FRAGMENT_COMPONENT_MANAGER,
CodeBlock.of("this"));
private static final Type VIEW =
new Type(
AndroidClassNames.VIEW_WITH_FRAGMENT_COMPONENT,
AndroidType.VIEW,
AndroidClassNames.VIEW_COMPONENT_MANAGER,
CodeBlock.of("this, true /* hasFragmentBindings */"));
private static final Type VIEW_NO_FRAGMENT =
new Type(
AndroidClassNames.VIEW_COMPONENT,
AndroidType.VIEW,
AndroidClassNames.VIEW_COMPONENT_MANAGER,
CodeBlock.of("this, false /* hasFragmentBindings */"));
final ClassName component;
final AndroidType androidType;
final ClassName manager;
final CodeBlock componentManagerInitArgs;
Type(
ClassName component,
AndroidType androidType,
ClassName manager,
CodeBlock componentManagerInitArgs) {
this.component = component;
this.androidType = androidType;
this.manager = manager;
this.componentManagerInitArgs = componentManagerInitArgs;
}
AndroidType androidType() {
return androidType;
}
private static Type of(TypeElement element, TypeElement baseElement) {
if (Processors.hasAnnotation(element, AndroidClassNames.HILT_ANDROID_APP)) {
return forHiltAndroidApp(element, baseElement);
}
return forAndroidEntryPoint(element, baseElement);
}
private static Type forHiltAndroidApp(TypeElement element, TypeElement baseElement) {
ProcessorErrors.checkState(
Processors.isAssignableFrom(baseElement, AndroidClassNames.APPLICATION),
element,
"@HiltAndroidApp base class must extend Application. Found: %s",
baseElement);
return Type.APPLICATION;
}
private static Type forAndroidEntryPoint(TypeElement element, TypeElement baseElement) {
if (Processors.isAssignableFrom(baseElement, AndroidClassNames.ACTIVITY)) {
ProcessorErrors.checkState(
Processors.isAssignableFrom(baseElement, AndroidClassNames.COMPONENT_ACTIVITY),
element,
"Activities annotated with @AndroidEntryPoint must be a subclass of "
+ "androidx.activity.ComponentActivity. (e.g. FragmentActivity, "
+ "AppCompatActivity, etc.)"
);
return Type.ACTIVITY;
} else if (Processors.isAssignableFrom(baseElement, AndroidClassNames.SERVICE)) {
return Type.SERVICE;
} else if (Processors.isAssignableFrom(baseElement, AndroidClassNames.BROADCAST_RECEIVER)) {
return Type.BROADCAST_RECEIVER;
} else if (Processors.isAssignableFrom(baseElement, AndroidClassNames.FRAGMENT)) {
return Type.FRAGMENT;
} else if (Processors.isAssignableFrom(baseElement, AndroidClassNames.VIEW)) {
boolean withFragmentBindings =
Processors.hasAnnotation(element, AndroidClassNames.WITH_FRAGMENT_BINDINGS);
return withFragmentBindings ? Type.VIEW : Type.VIEW_NO_FRAGMENT;
} else if (Processors.isAssignableFrom(baseElement, AndroidClassNames.APPLICATION)) {
throw new BadInputException(
"@AndroidEntryPoint cannot be used on an Application. Use @HiltAndroidApp instead.",
element);
}
throw new BadInputException(
"@AndroidEntryPoint base class must extend ComponentActivity, (support) Fragment, "
+ "View, Service, or BroadcastReceiver.",
element);
}
}
private static void checkConsistentAnnotations(
TypeElement element, AndroidEntryPointMetadata baseMetadata) {
TypeElement baseElement = baseMetadata.element();
checkAnnotationsMatch(element, baseElement, AndroidClassNames.WITH_FRAGMENT_BINDINGS);
ProcessorErrors.checkState(
baseMetadata.allowsOptionalInjection()
|| !Processors.hasAnnotation(element, AndroidClassNames.OPTIONAL_INJECT),
element,
"@OptionalInject Hilt class cannot extend from a non-optional @AndroidEntryPoint "
+ "base: %s",
element);
}
private static void checkAnnotationsMatch(
TypeElement element, TypeElement baseElement, ClassName annotationName) {
boolean isAnnotated = Processors.hasAnnotation(element, annotationName);
boolean isBaseAnnotated = Processors.hasAnnotation(baseElement, annotationName);
ProcessorErrors.checkState(
isAnnotated == isBaseAnnotated,
element,
isBaseAnnotated
? "Classes that extend an @%1$s base class must also be annotated @%1$s"
: "Classes that extend a @AndroidEntryPoint base class must not use @%1$s when the "
+ "base class "
+ "does not use @%1$s",
annotationName.simpleName());
}
}