dagger.internal.codegen.validation.InjectBindingRegistryImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dagger-compiler Show documentation
Show all versions of dagger-compiler Show documentation
A fast dependency injector for Android and Java.
The newest version!
/*
* Copyright (C) 2014 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.internal.codegen.validation;
import static androidx.room.compiler.processing.XElementKt.isTypeElement;
import static androidx.room.compiler.processing.compat.XConverters.toKS;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static dagger.internal.codegen.base.Keys.isValidImplicitProvisionKey;
import static dagger.internal.codegen.base.Keys.isValidMembersInjectionKey;
import static dagger.internal.codegen.binding.AssistedInjectionAnnotations.assistedInjectedConstructors;
import static dagger.internal.codegen.binding.InjectionAnnotations.hasInjectAnnotation;
import static dagger.internal.codegen.binding.InjectionAnnotations.injectedConstructors;
import static dagger.internal.codegen.binding.SourceFiles.generatedClassNameForBinding;
import static dagger.internal.codegen.extension.DaggerCollectors.toOptional;
import static dagger.internal.codegen.xprocessing.XElements.asTypeElement;
import static dagger.internal.codegen.xprocessing.XElements.closestEnclosingTypeElement;
import static dagger.internal.codegen.xprocessing.XTypes.erasedTypeName;
import static dagger.internal.codegen.xprocessing.XTypes.isDeclared;
import static dagger.internal.codegen.xprocessing.XTypes.nonObjectSuperclass;
import static dagger.internal.codegen.xprocessing.XTypes.unwrapType;
import androidx.room.compiler.processing.XConstructorElement;
import androidx.room.compiler.processing.XFieldElement;
import androidx.room.compiler.processing.XMessager;
import androidx.room.compiler.processing.XMethodElement;
import androidx.room.compiler.processing.XProcessingEnv;
import androidx.room.compiler.processing.XType;
import androidx.room.compiler.processing.XTypeElement;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.devtools.ksp.symbol.Origin;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.squareup.javapoet.ClassName;
import dagger.Component;
import dagger.Provides;
import dagger.internal.codegen.base.SourceFileGenerationException;
import dagger.internal.codegen.base.SourceFileGenerator;
import dagger.internal.codegen.binding.AssistedInjectionBinding;
import dagger.internal.codegen.binding.Binding;
import dagger.internal.codegen.binding.BindingFactory;
import dagger.internal.codegen.binding.ContributionBinding;
import dagger.internal.codegen.binding.InjectBindingRegistry;
import dagger.internal.codegen.binding.InjectionBinding;
import dagger.internal.codegen.binding.KeyFactory;
import dagger.internal.codegen.binding.MembersInjectionBinding;
import dagger.internal.codegen.binding.MembersInjectorBinding;
import dagger.internal.codegen.compileroption.CompilerOptions;
import dagger.internal.codegen.javapoet.TypeNames;
import dagger.internal.codegen.model.Key;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.tools.Diagnostic.Kind;
/**
* Maintains the collection of provision bindings from {@link Inject} constructors and members
* injection bindings from {@link Inject} fields and methods known to the annotation processor. Note
* that this registry does not handle any explicit bindings (those from {@link Provides}
* methods, {@link Component} dependencies, etc.).
*/
@Singleton
final class InjectBindingRegistryImpl implements InjectBindingRegistry {
private final XProcessingEnv processingEnv;
private final XMessager messager;
private final InjectValidator injectValidator;
private final InjectValidator injectValidatorWhenGeneratingCode;
private final KeyFactory keyFactory;
private final BindingFactory bindingFactory;
private final CompilerOptions compilerOptions;
private final class BindingsCollection {
private final ClassName factoryClass;
private final Map bindingsByKey = Maps.newLinkedHashMap();
private final Deque bindingsRequiringGeneration = new ArrayDeque<>();
private final Set materializedBindingKeys = Sets.newLinkedHashSet();
BindingsCollection(ClassName factoryClass) {
this.factoryClass = factoryClass;
}
void generateBindings(SourceFileGenerator generator) throws SourceFileGenerationException {
for (B binding = bindingsRequiringGeneration.poll();
binding != null;
binding = bindingsRequiringGeneration.poll()) {
checkState(!binding.unresolved().isPresent());
XType type = binding.key().type().xprocessing();
if (!isDeclared(type)
|| injectValidatorWhenGeneratingCode.validate(type.getTypeElement()).isClean()) {
generator.generate(binding);
}
materializedBindingKeys.add(binding.key());
}
// Because Elements instantiated across processing rounds are not guaranteed to be equals() to
// the logically same element, clear the cache after generating
bindingsByKey.clear();
}
/** Returns a previously cached binding. */
B getBinding(Key key) {
return bindingsByKey.get(key);
}
/** Caches the binding and generates it if it needs generation. */
void tryRegisterBinding(B binding, boolean isCalledFromInjectProcessor) {
if (processingEnv.getBackend() == XProcessingEnv.Backend.KSP) {
Origin origin =
toKS(closestEnclosingTypeElement(binding.bindingElement().get())).getOrigin();
// If the origin of the element is from a source file in the current compilation unit then
// we're guaranteed that the InjectProcessor should run over the element so only generate
// the Factory/MembersInjector if we're being called from the InjectProcessor.
//
// TODO(bcorso): generally, this isn't something we would need to keep track of manually.
// However, KSP incremental processing has a bug that will overwrite the cache for the
// element if we generate files for it, which can lead to missing generated files from
// other processors. See https://github.com/google/dagger/issues/4063 and
// https://github.com/google/dagger/issues/4054. Remove this once that bug is fixed.
if (!isCalledFromInjectProcessor && (origin == Origin.JAVA || origin == Origin.KOTLIN)) {
return;
}
}
tryToCacheBinding(binding);
@SuppressWarnings("unchecked")
B maybeUnresolved =
binding.unresolved().isPresent() ? (B) binding.unresolved().get() : binding;
tryToGenerateBinding(maybeUnresolved, isCalledFromInjectProcessor);
}
/**
* Tries to generate a binding, not generating if it already is generated. For resolved
* bindings, this will try to generate the unresolved version of the binding.
*/
void tryToGenerateBinding(B binding, boolean isCalledFromInjectProcessor) {
if (shouldGenerateBinding(binding)) {
bindingsRequiringGeneration.offer(binding);
if (compilerOptions.warnIfInjectionFactoryNotGeneratedUpstream()
&& !isCalledFromInjectProcessor) {
messager.printMessage(
Kind.NOTE,
String.format(
"Generating a %s for %s. "
+ "Prefer to run the dagger processor over that class instead.",
factoryClass.simpleName(),
// erasure to strip from msgs.
erasedTypeName(binding.key().type().xprocessing())));
}
}
}
/** Returns true if the binding needs to be generated. */
private boolean shouldGenerateBinding(B binding) {
if (binding instanceof MembersInjectionBinding) {
MembersInjectionBinding membersInjectionBinding = (MembersInjectionBinding) binding;
// Empty members injection bindings are special and don't need source files.
if (membersInjectionBinding.injectionSites().isEmpty()) {
return false;
}
// Members injectors for classes with no local injection sites and no @Inject
// constructor are unused.
boolean hasInjectConstructor =
!(injectedConstructors(membersInjectionBinding.membersInjectedType()).isEmpty()
&& assistedInjectedConstructors(
membersInjectionBinding.membersInjectedType()).isEmpty());
if (!membersInjectionBinding.hasLocalInjectionSites() && !hasInjectConstructor) {
return false;
}
}
return !binding.unresolved().isPresent()
&& !materializedBindingKeys.contains(binding.key())
&& !bindingsRequiringGeneration.contains(binding)
&& processingEnv.findTypeElement(generatedClassNameForBinding(binding)) == null;
}
/** Caches the binding for future lookups by key. */
private void tryToCacheBinding(B binding) {
// We only cache resolved bindings or unresolved bindings w/o type arguments.
// Unresolved bindings w/ type arguments aren't valid for the object graph.
if (binding.unresolved().isPresent()
|| binding.bindingTypeElement().get().getType().getTypeArguments().isEmpty()) {
Key key = binding.key();
Binding previousValue = bindingsByKey.put(key, binding);
checkState(previousValue == null || binding.equals(previousValue),
"couldn't register %s. %s was already registered for %s",
binding, previousValue, key);
}
}
}
private final BindingsCollection injectionBindings =
new BindingsCollection<>(TypeNames.PROVIDER);
private final BindingsCollection membersInjectionBindings =
new BindingsCollection<>(TypeNames.MEMBERS_INJECTOR);
@Inject
InjectBindingRegistryImpl(
XProcessingEnv processingEnv,
XMessager messager,
InjectValidator injectValidator,
KeyFactory keyFactory,
BindingFactory bindingFactory,
CompilerOptions compilerOptions) {
this.processingEnv = processingEnv;
this.messager = messager;
this.injectValidator = injectValidator;
this.injectValidatorWhenGeneratingCode = injectValidator.whenGeneratingCode();
this.keyFactory = keyFactory;
this.bindingFactory = bindingFactory;
this.compilerOptions = compilerOptions;
}
// TODO(dpb): make the SourceFileGenerators fields so they don't have to be passed in
@Override
public void generateSourcesForRequiredBindings(
SourceFileGenerator factoryGenerator,
SourceFileGenerator membersInjectorGenerator)
throws SourceFileGenerationException {
injectionBindings.generateBindings(factoryGenerator);
membersInjectionBindings.generateBindings(membersInjectorGenerator);
}
@Override
public Optional tryRegisterInjectConstructor(
XConstructorElement constructorElement) {
return tryRegisterConstructor(
constructorElement,
Optional.empty(),
/* isCalledFromInjectProcessor= */ true);
}
@CanIgnoreReturnValue
private Optional tryRegisterConstructor(
XConstructorElement constructorElement,
Optional resolvedType,
boolean isCalledFromInjectProcessor) {
XTypeElement typeElement = constructorElement.getEnclosingElement();
// Validating here shouldn't have a performance penalty because the validator caches its reports
ValidationReport report = injectValidator.validate(typeElement);
report.printMessagesTo(messager);
if (!report.isClean()) {
return Optional.empty();
}
XType type = typeElement.getType();
Key key = keyFactory.forInjectConstructorWithResolvedType(type);
ContributionBinding cachedBinding = injectionBindings.getBinding(key);
if (cachedBinding != null) {
return Optional.of(cachedBinding);
}
if (hasInjectAnnotation(constructorElement)) {
InjectionBinding binding = bindingFactory.injectionBinding(constructorElement, resolvedType);
injectionBindings.tryRegisterBinding(binding, isCalledFromInjectProcessor);
if (!binding.injectionSites().isEmpty()) {
tryRegisterMembersInjectedType(typeElement, resolvedType, isCalledFromInjectProcessor);
}
return Optional.of(binding);
} else if (constructorElement.hasAnnotation(TypeNames.ASSISTED_INJECT)) {
AssistedInjectionBinding binding =
bindingFactory.assistedInjectionBinding(constructorElement, resolvedType);
injectionBindings.tryRegisterBinding(binding, isCalledFromInjectProcessor);
if (!binding.injectionSites().isEmpty()) {
tryRegisterMembersInjectedType(typeElement, resolvedType, isCalledFromInjectProcessor);
}
return Optional.of(binding);
}
throw new AssertionError(
"Expected either an @Inject or @AssistedInject annotated constructor: "
+ constructorElement.getEnclosingElement().getQualifiedName());
}
@Override
public Optional tryRegisterInjectField(XFieldElement fieldElement) {
// TODO(b/204116636): Add a test for this once we're able to test kotlin sources.
// TODO(b/204208307): Add validation for KAPT to test if this came from a top-level field.
if (!isTypeElement(fieldElement.getEnclosingElement())) {
messager.printMessage(
Kind.ERROR,
"@Inject fields must be enclosed in a type.",
fieldElement);
}
return tryRegisterMembersInjectedType(
asTypeElement(fieldElement.getEnclosingElement()),
Optional.empty(),
/* isCalledFromInjectProcessor= */ true);
}
@Override
public Optional tryRegisterInjectMethod(XMethodElement methodElement) {
// TODO(b/204116636): Add a test for this once we're able to test kotlin sources.
// TODO(b/204208307): Add validation for KAPT to test if this came from a top-level method.
if (!isTypeElement(methodElement.getEnclosingElement())) {
messager.printMessage(
Kind.ERROR,
"@Inject methods must be enclosed in a type.",
methodElement);
}
return tryRegisterMembersInjectedType(
asTypeElement(methodElement.getEnclosingElement()),
Optional.empty(),
/* isCalledFromInjectProcessor= */ true);
}
@CanIgnoreReturnValue
private Optional tryRegisterMembersInjectedType(
XTypeElement typeElement,
Optional resolvedType,
boolean isCalledFromInjectProcessor) {
// Validating here shouldn't have a performance penalty because the validator caches its reports
ValidationReport report = injectValidator.validateForMembersInjection(typeElement);
report.printMessagesTo(messager);
if (!report.isClean()) {
return Optional.empty();
}
XType type = typeElement.getType();
Key key = keyFactory.forInjectConstructorWithResolvedType(type);
MembersInjectionBinding cachedBinding = membersInjectionBindings.getBinding(key);
if (cachedBinding != null) {
return Optional.of(cachedBinding);
}
MembersInjectionBinding binding = bindingFactory.membersInjectionBinding(type, resolvedType);
membersInjectionBindings.tryRegisterBinding(binding, isCalledFromInjectProcessor);
for (Optional supertype = nonObjectSuperclass(type);
supertype.isPresent();
supertype = nonObjectSuperclass(supertype.get())) {
getOrFindMembersInjectionBinding(keyFactory.forMembersInjectedType(supertype.get()));
}
return Optional.of(binding);
}
@CanIgnoreReturnValue
@Override
public Optional getOrFindInjectionBinding(Key key) {
checkNotNull(key);
if (!isValidImplicitProvisionKey(key)) {
return Optional.empty();
}
ContributionBinding binding = injectionBindings.getBinding(key);
if (binding != null) {
return Optional.of(binding);
}
XType type = key.type().xprocessing();
XTypeElement element = type.getTypeElement();
ValidationReport report = injectValidator.validate(element);
report.printMessagesTo(messager);
if (!report.isClean()) {
return Optional.empty();
}
return Stream.concat(
injectedConstructors(element).stream(),
assistedInjectedConstructors(element).stream())
// We're guaranteed that there's at most 1 @Inject constructors from above validation.
.collect(toOptional())
.flatMap(
constructor ->
tryRegisterConstructor(
constructor,
Optional.of(type),
/* isCalledFromInjectProcessor= */ false));
}
@CanIgnoreReturnValue
@Override
public Optional getOrFindMembersInjectionBinding(Key key) {
checkNotNull(key);
// TODO(gak): is checking the kind enough?
checkArgument(isValidMembersInjectionKey(key));
MembersInjectionBinding binding = membersInjectionBindings.getBinding(key);
if (binding != null) {
return Optional.of(binding);
}
return tryRegisterMembersInjectedType(
key.type().xprocessing().getTypeElement(),
Optional.of(key.type().xprocessing()),
/* isCalledFromInjectProcessor= */ false);
}
@Override
public Optional getOrFindMembersInjectorBinding(Key key) {
if (!isValidMembersInjectionKey(key)) {
return Optional.empty();
}
Key membersInjectionKey =
keyFactory.forMembersInjectedType(unwrapType(key.type().xprocessing()));
return getOrFindMembersInjectionBinding(membersInjectionKey)
.map(binding -> bindingFactory.membersInjectorBinding(key, binding));
}
}