/* * 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 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) || injectValidator.validateWhenGeneratingCode(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.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)); } }